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
|
-
//
|
|
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
|
-
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
//
|
|
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
|
-
});
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
255
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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) {
|