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
|
-
//
|
|
147
|
+
// Parse and execute ALL blocks automatically
|
|
148
148
|
try {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
//
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
271
|
-
|
|
261
|
+
catch (waitError) {
|
|
262
|
+
// Continue even if wait fails - element might already be ready
|
|
272
263
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
296
|
-
|
|
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
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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) {
|