n8n-nodes-nvk-browser 1.0.1 → 1.0.4

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,23 @@ exports.moveAndClickFields = [
22
22
  type: 'string',
23
23
  required: true,
24
24
  default: '',
25
- description: 'CSS selector of the element to click',
25
+ description: 'For Puppeteer: CSS selector, XPath (with >XPATH> prefix), or Locator.race() code block. Examples: "textarea", ">XPATH>/html/body/div", or paste the Locator.race() block (e.g., "puppeteer.Locator.race([targetPage.locator(\'selector\')]).click()"). The node will automatically extract locators from the first .click() block. For Javascript: CSS selector only.',
26
+ typeOptions: {
27
+ rows: 4,
28
+ },
29
+ displayOptions: {
30
+ show: {
31
+ resource: ['page'],
32
+ operation: ['moveAndClick'],
33
+ },
34
+ },
35
+ },
36
+ {
37
+ displayName: 'Timeout (Milliseconds)',
38
+ name: 'timeout',
39
+ type: 'number',
40
+ default: 30000,
41
+ description: 'Timeout in milliseconds for waiting for the element',
26
42
  displayOptions: {
27
43
  show: {
28
44
  resource: ['page'],
@@ -35,10 +51,6 @@ exports.moveAndClickFields = [
35
51
  name: 'clickMethod',
36
52
  type: 'options',
37
53
  options: [
38
- {
39
- name: 'Use GhostCursor',
40
- value: 'ghostcursor',
41
- },
42
54
  {
43
55
  name: 'Use Puppeteer',
44
56
  value: 'puppeteer',
@@ -61,13 +73,13 @@ exports.moveAndClickFields = [
61
73
  displayName: 'Wait For Click (Milliseconds)',
62
74
  name: 'waitForClick',
63
75
  type: 'number',
64
- default: 0,
76
+ default: 500,
65
77
  description: 'Wait time before clicking in milliseconds',
66
78
  displayOptions: {
67
79
  show: {
68
80
  resource: ['page'],
69
81
  operation: ['moveAndClick'],
70
- clickMethod: ['javascript'],
82
+ clickMethod: ['puppeteer'],
71
83
  },
72
84
  },
73
85
  },
@@ -95,7 +107,7 @@ exports.moveAndClickFields = [
95
107
  show: {
96
108
  resource: ['page'],
97
109
  operation: ['moveAndClick'],
98
- clickMethod: ['javascript'],
110
+ clickMethod: ['puppeteer'],
99
111
  },
100
112
  },
101
113
  },
@@ -112,7 +124,7 @@ exports.moveAndClickFields = [
112
124
  show: {
113
125
  resource: ['page'],
114
126
  operation: ['moveAndClick'],
115
- clickMethod: ['javascript'],
127
+ clickMethod: ['puppeteer'],
116
128
  },
117
129
  },
118
130
  },
@@ -22,11 +22,15 @@ var __importStar = (this && this.__importStar) || function (mod) {
22
22
  __setModuleDefault(result, mod);
23
23
  return result;
24
24
  };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
25
28
  Object.defineProperty(exports, "__esModule", { value: true });
26
29
  exports.MoveAndClick = void 0;
27
30
  const BrowserManager_1 = require("../../../utils/BrowserManager");
28
31
  const MoveAndClick_description_1 = require("./MoveAndClick.description");
29
32
  const path = __importStar(require("path"));
33
+ const puppeteer_core_1 = __importDefault(require("puppeteer-core"));
30
34
  class MoveAndClick {
31
35
  constructor() {
32
36
  this.description = {
@@ -92,9 +96,7 @@ class MoveAndClick {
92
96
  const profileId = this.getNodeParameter('profileId', i);
93
97
  const selector = this.getNodeParameter('selector', i);
94
98
  const clickMethod = this.getNodeParameter('clickMethod', i) || 'puppeteer';
95
- const waitForClick = this.getNodeParameter('waitForClick', i) || 0;
96
- const button = this.getNodeParameter('button', i) || 'left';
97
- const clickCount = this.getNodeParameter('clickCount', i) || 1;
99
+ const timeout = this.getNodeParameter('timeout', i) || 30000;
98
100
  const tabIndex = this.getNodeParameter('tabIndex', i) || 0;
99
101
  const autoStart = this.getNodeParameter('autoStart', i) || false;
100
102
  let instance = browserManager.getInstance(profileId);
@@ -109,50 +111,238 @@ class MoveAndClick {
109
111
  if (!page) {
110
112
  throw new Error(`Could not get page for profile ${profileId}`);
111
113
  }
112
- // Wait for element if needed
113
- await page.waitForSelector(selector, { timeout: 30000 });
114
+ // Set default timeout
115
+ page.setDefaultTimeout(timeout);
114
116
  if (clickMethod === 'javascript') {
115
- // Wait before clicking
116
- if (waitForClick > 0) {
117
- await page.waitForTimeout(waitForClick);
118
- }
117
+ // Use Javascript Click - simple implementation
118
+ await page.waitForSelector(selector, { timeout });
119
119
  // JavaScript click
120
- const buttonMap = {
121
- left: 0,
122
- middle: 1,
123
- right: 2,
124
- };
125
- await page.evaluate((sel, btn, count) => {
120
+ await page.evaluate((sel) => {
126
121
  // This code runs in browser context, so DOM APIs are available
127
122
  // eslint-disable-next-line @typescript-eslint/no-implied-eval
128
123
  const element = document.querySelector(sel);
129
124
  if (element) {
130
- for (let i = 0; i < count; i++) {
131
- const event = new MouseEvent('click', {
132
- view: window,
133
- bubbles: true,
134
- cancelable: true,
135
- button: btn,
136
- });
137
- element.dispatchEvent(event);
138
- }
125
+ const event = new MouseEvent('click', {
126
+ view: window,
127
+ bubbles: true,
128
+ cancelable: true,
129
+ button: 0,
130
+ });
131
+ element.dispatchEvent(event);
139
132
  }
140
- }, selector, buttonMap[button] || 0, clickCount);
133
+ }, selector);
141
134
  }
142
135
  else if (clickMethod === 'puppeteer') {
143
- // Puppeteer native click
144
- await page.click(selector);
145
- }
146
- else if (clickMethod === 'ghostcursor') {
147
- // GhostCursor simulation (simplified - would need ghost-cursor package)
148
- const element = await page.$(selector);
149
- if (element) {
150
- const box = await element.boundingBox();
151
- if (box) {
152
- // Move mouse to center of element
153
- await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
154
- await page.waitForTimeout(100);
155
- await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
136
+ // Use Puppeteer - support Locator.race() pattern
137
+ const waitForClick = this.getNodeParameter('waitForClick', i) || 500;
138
+ const button = this.getNodeParameter('button', i) || 'left';
139
+ 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
+ // Check if selector contains Locator.race pattern or is a code block
143
+ if (selectorTrimmed.includes('Locator.race') || selectorTrimmed.includes('puppeteer.Locator')) {
144
+ // Execute as code - user provided full code
145
+ // Extract the selector part from the code or execute the code directly
146
+ try {
147
+ // Parse selectors from the code pattern
148
+ const locators = [];
149
+ // Find Locator.race blocks - prioritize blocks that end with .click()
150
+ // First, try to find blocks with .click()
151
+ const clickBlocks = selector.match(/puppeteer\.Locator\.race\s*\(\s*\[([\s\S]*?)\]\s*\)[\s\S]*?\.click\s*\(/g);
152
+ let raceBlockContent = null;
153
+ if (clickBlocks && clickBlocks.length > 0) {
154
+ // Use the first click block
155
+ const firstClickBlock = clickBlocks[0];
156
+ const match = firstClickBlock.match(/puppeteer\.Locator\.race\s*\(\s*\[([\s\S]*?)\]\s*\)/);
157
+ if (match && match[1]) {
158
+ raceBlockContent = match[1];
159
+ }
160
+ }
161
+ else {
162
+ // Fallback: find any Locator.race block
163
+ const anyRaceBlockMatch = selector.match(/puppeteer\.Locator\.race\s*\(\s*\[([\s\S]*?)\]\s*\)/);
164
+ if (anyRaceBlockMatch && anyRaceBlockMatch[1]) {
165
+ raceBlockContent = anyRaceBlockMatch[1];
166
+ }
167
+ }
168
+ if (raceBlockContent) {
169
+ // Extract all locator calls from the race block
170
+ // Handle both single and double quotes, and handle escaped quotes
171
+ const locatorMatches = raceBlockContent.match(/(?:targetPage|page)\.locator\(['"]([^'"]*(?:\\.[^'"]*)*)['"]\)/g);
172
+ if (locatorMatches) {
173
+ locatorMatches.forEach(match => {
174
+ // Extract selector value, handling escaped quotes
175
+ const selectorMatch = match.match(/['"]([^'"]*(?:\\.[^'"]*)*)['"]/);
176
+ if (selectorMatch && selectorMatch[1]) {
177
+ const selectorValue = selectorMatch[1].replace(/\\(.)/g, '$1'); // Unescape
178
+ if (selectorValue && typeof page.locator === 'function') {
179
+ locators.push(page.locator(selectorValue));
180
+ }
181
+ }
182
+ });
183
+ }
184
+ }
185
+ if (locators.length === 0) {
186
+ // Fallback: Parse CSS selectors (lines with targetPage.locator('...') or page.locator('...'))
187
+ const cssMatches = selector.match(/(?:targetPage|page)\.locator\(['"]([^'"]+)['"]\)/g);
188
+ if (cssMatches) {
189
+ cssMatches.forEach(match => {
190
+ const cssSel = match.match(/['"]([^'"]+)['"]/)?.[1];
191
+ if (cssSel && !cssSel.startsWith('::-p-xpath')) {
192
+ // Use page.locator if available, otherwise fallback
193
+ if (typeof page.locator === 'function') {
194
+ locators.push(page.locator(cssSel));
195
+ }
196
+ }
197
+ });
198
+ }
199
+ // Parse XPath selectors (::-p-xpath(...))
200
+ const xpathMatches = selector.match(/::-p-xpath\(([^)]+)\)/g);
201
+ if (xpathMatches) {
202
+ xpathMatches.forEach(match => {
203
+ const xpath = match.match(/::-p-xpath\(([^)]+)\)/)?.[1];
204
+ if (xpath && typeof page.locator === 'function') {
205
+ locators.push(page.locator(`::-p-xpath(${xpath})`));
206
+ }
207
+ });
208
+ }
209
+ }
210
+ // Also check for >CSS> and >XPATH> prefixes
211
+ if (selector.includes('>CSS>') || selector.includes('>XPATH>')) {
212
+ const parts = selector.split(/\s+/);
213
+ parts.forEach(part => {
214
+ if (part.startsWith('>CSS>') && typeof page.locator === 'function') {
215
+ const cssSel = part.replace('>CSS>', '');
216
+ locators.push(page.locator(cssSel));
217
+ }
218
+ else if (part.startsWith('>XPATH>') && typeof page.locator === 'function') {
219
+ const xpath = part.replace('>XPATH>', '');
220
+ locators.push(page.locator(`::-p-xpath(${xpath})`));
221
+ }
222
+ });
223
+ }
224
+ if (locators.length > 0 && typeof page.locator === 'function') {
225
+ // Use Locator.race() if multiple locators and API is available
226
+ let locator;
227
+ if (locators.length > 1) {
228
+ // Check if Locator.race is available
229
+ const LocatorClass = puppeteer_core_1.default.Locator;
230
+ if (LocatorClass && typeof LocatorClass.race === 'function') {
231
+ locator = LocatorClass.race(locators);
232
+ if (typeof locator.setTimeout === 'function') {
233
+ locator = locator.setTimeout(timeout);
234
+ }
235
+ }
236
+ else {
237
+ // Fallback: try first locator
238
+ locator = locators[0];
239
+ if (typeof locator.setTimeout === 'function') {
240
+ locator = locator.setTimeout(timeout);
241
+ }
242
+ }
243
+ }
244
+ else {
245
+ locator = locators[0];
246
+ if (typeof locator.setTimeout === 'function') {
247
+ locator = locator.setTimeout(timeout);
248
+ }
249
+ }
250
+ // Wait before clicking if specified
251
+ if (waitForClick > 0) {
252
+ await page.waitForTimeout(waitForClick);
253
+ }
254
+ // Parse offset from code if present
255
+ let offset;
256
+ const offsetMatch = selector.match(/offset:\s*\{\s*x:\s*(\d+),\s*y:\s*(\d+)\s*\}/);
257
+ if (offsetMatch) {
258
+ offset = {
259
+ x: parseInt(offsetMatch[1], 10),
260
+ y: parseInt(offsetMatch[2], 10),
261
+ };
262
+ }
263
+ // Click with options
264
+ const clickOptions = {
265
+ button: button,
266
+ clickCount: clickCount,
267
+ };
268
+ // Add offset if found in code
269
+ if (offset) {
270
+ clickOptions.offset = offset;
271
+ }
272
+ if (typeof locator.click === 'function') {
273
+ await locator.click(clickOptions);
274
+ }
275
+ else {
276
+ throw new Error('Locator.click is not available');
277
+ }
278
+ }
279
+ else {
280
+ // Fallback to simple selector parsing
281
+ 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.');
282
+ }
283
+ }
284
+ catch (error) {
285
+ // If code parsing fails, provide helpful error message
286
+ const errorMessage = error instanceof Error ? error.message : String(error);
287
+ throw new Error(`Failed to parse Puppeteer Locator code: ${errorMessage}. Please check that your selector field contains a valid Locator.race() pattern or a simple CSS/XPath selector.`);
288
+ }
289
+ }
290
+ else {
291
+ // Simple selector - parse CSS or XPath prefixes
292
+ // Try to use Locator API if available, otherwise fallback to traditional API
293
+ if (typeof page.locator === 'function') {
294
+ let locator;
295
+ if (selectorTrimmed.startsWith('>CSS>')) {
296
+ const cssSel = selectorTrimmed.replace('>CSS>', '').trim();
297
+ locator = page.locator(cssSel);
298
+ }
299
+ else if (selectorTrimmed.startsWith('>XPATH>')) {
300
+ const xpath = selectorTrimmed.replace('>XPATH>', '').trim();
301
+ locator = page.locator(`::-p-xpath(${xpath})`);
302
+ }
303
+ else if (selectorTrimmed.startsWith('::-p-xpath')) {
304
+ // Already in XPath format
305
+ locator = page.locator(selectorTrimmed);
306
+ }
307
+ else {
308
+ // Default to CSS selector
309
+ locator = page.locator(selectorTrimmed);
310
+ }
311
+ // Set timeout if method exists
312
+ if (typeof locator.setTimeout === 'function') {
313
+ locator = locator.setTimeout(timeout);
314
+ }
315
+ // Wait before clicking if specified
316
+ if (waitForClick > 0) {
317
+ await page.waitForTimeout(waitForClick);
318
+ }
319
+ // Click with options
320
+ const clickOptions = {
321
+ button: button,
322
+ clickCount: clickCount,
323
+ };
324
+ if (typeof locator.click === 'function') {
325
+ await locator.click(clickOptions);
326
+ }
327
+ else {
328
+ // Fallback to traditional API
329
+ await page.waitForSelector(selectorTrimmed, { timeout });
330
+ if (waitForClick > 0) {
331
+ await page.waitForTimeout(waitForClick);
332
+ }
333
+ await page.click(selectorTrimmed, clickOptions);
334
+ }
335
+ }
336
+ else {
337
+ // Fallback to traditional API
338
+ await page.waitForSelector(selectorTrimmed, { timeout });
339
+ if (waitForClick > 0) {
340
+ await page.waitForTimeout(waitForClick);
341
+ }
342
+ await page.click(selectorTrimmed, {
343
+ button: button,
344
+ clickCount: clickCount,
345
+ });
156
346
  }
157
347
  }
158
348
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-nvk-browser",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "n8n nodes for managing Chrome browser profiles and page interactions",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",