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
|
|
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
|
-
//
|
|
147
|
+
// Parse and execute ALL blocks automatically
|
|
146
148
|
try {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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) {
|