@hanivanrizky/nestjs-browser-action 0.21.0 → 0.22.0

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.
Files changed (73) hide show
  1. package/README.md +72 -31
  2. package/dist/browser-action.module-definition.d.ts +1 -0
  3. package/dist/browser-action.module.d.ts +3 -0
  4. package/dist/browser-action.module.js +146 -27
  5. package/dist/browser-action.module.js.map +1 -1
  6. package/dist/common/index.d.ts +1 -0
  7. package/dist/common/index.js +18 -0
  8. package/dist/common/index.js.map +1 -0
  9. package/dist/common/tokens.d.ts +7 -0
  10. package/dist/common/tokens.js +36 -0
  11. package/dist/common/tokens.js.map +1 -0
  12. package/dist/decorators/index.d.ts +1 -0
  13. package/dist/decorators/index.js +1 -0
  14. package/dist/decorators/index.js.map +1 -1
  15. package/dist/decorators/inject-browser.decorator.d.ts +2 -1
  16. package/dist/decorators/inject-browser.decorator.js +2 -1
  17. package/dist/decorators/inject-browser.decorator.js.map +1 -1
  18. package/dist/decorators/inject-page-controller.decorator.d.ts +2 -0
  19. package/dist/decorators/inject-page-controller.decorator.js +8 -0
  20. package/dist/decorators/inject-page-controller.decorator.js.map +1 -0
  21. package/dist/decorators/inject-page.decorator.d.ts +2 -1
  22. package/dist/decorators/inject-page.decorator.js +2 -1
  23. package/dist/decorators/inject-page.decorator.js.map +1 -1
  24. package/dist/examples/named-browser-example.d.ts +1 -0
  25. package/dist/examples/named-browser-example.js +183 -0
  26. package/dist/examples/named-browser-example.js.map +1 -0
  27. package/dist/index.d.ts +5 -0
  28. package/dist/index.js +5 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/interfaces/browser-action-options.d.ts +1 -0
  31. package/dist/operators/capture.operator.d.ts +11 -0
  32. package/dist/operators/capture.operator.js +62 -0
  33. package/dist/operators/capture.operator.js.map +1 -0
  34. package/dist/operators/container.operator.d.ts +13 -0
  35. package/dist/operators/container.operator.js +149 -0
  36. package/dist/operators/container.operator.js.map +1 -0
  37. package/dist/operators/extraction.operator.d.ts +22 -0
  38. package/dist/operators/extraction.operator.js +204 -0
  39. package/dist/operators/extraction.operator.js.map +1 -0
  40. package/dist/operators/index.d.ts +5 -0
  41. package/dist/operators/index.js +14 -0
  42. package/dist/operators/index.js.map +1 -0
  43. package/dist/operators/pagination.operator.d.ts +24 -0
  44. package/dist/operators/pagination.operator.js +182 -0
  45. package/dist/operators/pagination.operator.js.map +1 -0
  46. package/dist/operators/workflow.operator.d.ts +35 -0
  47. package/dist/operators/workflow.operator.js +501 -0
  48. package/dist/operators/workflow.operator.js.map +1 -0
  49. package/dist/services/browser-action.service.d.ts +6 -29
  50. package/dist/services/browser-action.service.js +28 -968
  51. package/dist/services/browser-action.service.js.map +1 -1
  52. package/dist/services/browser-holder.d.ts +7 -0
  53. package/dist/services/browser-holder.js +26 -0
  54. package/dist/services/browser-holder.js.map +1 -0
  55. package/dist/services/browser-manager.service.js.map +1 -1
  56. package/dist/services/browser-pool.service.d.ts +0 -4
  57. package/dist/services/browser-pool.service.js +12 -103
  58. package/dist/services/browser-pool.service.js.map +1 -1
  59. package/dist/services/named-browser-shutdown.d.ts +9 -0
  60. package/dist/services/named-browser-shutdown.js +47 -0
  61. package/dist/services/named-browser-shutdown.js.map +1 -0
  62. package/dist/services/page-controller.d.ts +39 -0
  63. package/dist/services/page-controller.js +325 -0
  64. package/dist/services/page-controller.js.map +1 -0
  65. package/dist/services/page-session.d.ts +26 -0
  66. package/dist/services/page-session.js +135 -0
  67. package/dist/services/page-session.js.map +1 -0
  68. package/dist/services/page.service.js.map +1 -1
  69. package/dist/tsconfig.build.tsbuildinfo +1 -1
  70. package/dist/utils/browser-launcher.d.ts +6 -0
  71. package/dist/utils/browser-launcher.js +100 -0
  72. package/dist/utils/browser-launcher.js.map +1 -0
  73. package/package.json +2 -2
@@ -16,30 +16,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
16
16
  exports.BrowserActionService = void 0;
17
17
  const common_1 = require("@nestjs/common");
18
18
  const core_1 = require("@nestjs/core");
19
- const fs_1 = require("fs");
20
- const path_1 = require("path");
21
- const class_validator_1 = require("class-validator");
22
19
  const path_util_1 = require("../utils/path.util");
23
20
  const workflow_validator_1 = require("../validators/workflow.validator");
24
21
  const page_service_1 = require("./page.service");
25
22
  const cookie_service_1 = require("./cookie.service");
26
23
  const cleansing_service_1 = require("./cleansing.service");
27
24
  const pipe_engine_1 = require("../pipes/pipe-engine");
25
+ const operators_1 = require("../operators");
28
26
  const user_agent_util_1 = require("../utils/user-agent.util");
29
27
  const logger_util_1 = require("../utils/logger.util");
30
- const delay_util_1 = require("../utils/delay.util");
31
28
  const dom_util_1 = require("../utils/dom.util");
32
29
  const truncate_log_util_1 = require("../utils/truncate-log.util");
33
30
  const browser_action_constants_1 = require("../constants/browser-action.constants");
34
- const isTargetClosed = (err) => err instanceof Error &&
35
- (err.message.includes('Target closed') ||
36
- err.message.includes('No target with given id') ||
37
- err.message.includes('detached Frame') ||
38
- err.message.includes('Attempted to use detached') ||
39
- err.message.includes('Execution context was destroyed') ||
40
- err.message.includes('context was destroyed') ||
41
- err.message.includes('Session closed') ||
42
- err.message.includes('Navigating frame was detached'));
43
31
  let BrowserActionService = BrowserActionService_1 = class BrowserActionService {
44
32
  pageService;
45
33
  cookieService;
@@ -49,6 +37,11 @@ let BrowserActionService = BrowserActionService_1 = class BrowserActionService {
49
37
  logger;
50
38
  pipeEngine = new pipe_engine_1.PipeEngine();
51
39
  activeDebugLogMaxLength;
40
+ extraction;
41
+ capture;
42
+ container;
43
+ workflow;
44
+ pagination;
52
45
  constructor(pageService, cookieService, cleansingService, moduleRef, moduleOptions) {
53
46
  this.pageService = pageService;
54
47
  this.cookieService = cookieService;
@@ -58,6 +51,11 @@ let BrowserActionService = BrowserActionService_1 = class BrowserActionService {
58
51
  this.logger = new logger_util_1.LoggerWithLevel(BrowserActionService_1.name, this.pageService.getLogLevel());
59
52
  this.activeDebugLogMaxLength =
60
53
  moduleOptions?.debugLogMaxLength ?? browser_action_constants_1.DEFAULT_DEBUG_LOG_MAX_LENGTH;
54
+ this.extraction = new operators_1.ExtractionOperator(this.pipeEngine, this.logger);
55
+ this.capture = new operators_1.CaptureOperator(this.logger);
56
+ this.container = new operators_1.ContainerOperator(this.extraction, this.pipeEngine, this.logger);
57
+ this.workflow = new operators_1.WorkflowOperator(this.extraction, this.container, this.pipeEngine, this.cleansingService, this.logger, () => this.cookieService);
58
+ this.pagination = new operators_1.PaginationOperator(this.logger);
61
59
  }
62
60
  async newPage() {
63
61
  return this.moduleRef.resolve(page_service_1.PageService, undefined, { strict: false });
@@ -70,8 +68,7 @@ let BrowserActionService = BrowserActionService_1 = class BrowserActionService {
70
68
  const pageService = await this.newPage();
71
69
  const page = await pageService.navigateTo(url, this.buildNavOptions(scraperOptions), cloak, scraperOptions?.interceptResource);
72
70
  try {
73
- const result = await page.screenshot({ path, ...options });
74
- return result;
71
+ return await this.capture.screenshot(page, path, options);
75
72
  }
76
73
  finally {
77
74
  await pageService.closePage();
@@ -85,8 +82,7 @@ let BrowserActionService = BrowserActionService_1 = class BrowserActionService {
85
82
  const pageService = await this.newPage();
86
83
  const page = await pageService.navigateTo(url, this.buildNavOptions(scraperOptions), cloak, scraperOptions?.interceptResource);
87
84
  try {
88
- const pdf = await page.pdf({ path, ...options });
89
- return Buffer.from(pdf);
85
+ return await this.capture.pdf(page, path, options);
90
86
  }
91
87
  finally {
92
88
  await pageService.closePage();
@@ -96,51 +92,12 @@ let BrowserActionService = BrowserActionService_1 = class BrowserActionService {
96
92
  this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, `Capturing TLS fingerprint from ${url}`));
97
93
  const pageService = await this.newPage();
98
94
  const page = await pageService.navigateTo(url);
99
- let rawJson;
100
95
  try {
101
- rawJson = await page.evaluate(() => document.body.innerText);
96
+ return await this.capture.tlsFingerprint(page, path);
102
97
  }
103
98
  finally {
104
99
  await pageService.closePage();
105
100
  }
106
- const data = JSON.parse(rawJson);
107
- const fingerprint = this.buildTlsFingerprint(data);
108
- await fs_1.promises.mkdir((0, path_1.dirname)(path), { recursive: true });
109
- await fs_1.promises.writeFile(path, JSON.stringify(fingerprint, null, 2), 'utf-8');
110
- this.logger.log(`Saved TLS fingerprint to ${path}`);
111
- return fingerprint;
112
- }
113
- buildTlsFingerprint(data) {
114
- const tls = (data.tls ?? {});
115
- const http2 = (data.http2 ?? {});
116
- const extensions = Array.isArray(tls.extensions)
117
- ? tls.extensions
118
- : [];
119
- const sentFrames = Array.isArray(http2.sent_frames)
120
- ? http2.sent_frames
121
- : [];
122
- const headersFrame = sentFrames.find((frame) => frame.frame_type === 'HEADERS');
123
- return {
124
- capturedAt: new Date().toISOString(),
125
- ip: data.ip ?? '',
126
- httpVersion: data.http_version ?? '',
127
- method: data.method ?? '',
128
- userAgent: data.user_agent ?? '',
129
- ja3: tls.ja3 ?? '',
130
- ja3Hash: tls.ja3_hash ?? '',
131
- ja4: tls.ja4 ?? '',
132
- ja4_r: tls.ja4_r,
133
- peetprint: tls.peetprint ?? '',
134
- peetprintHash: tls.peetprint_hash ?? '',
135
- ciphers: Array.isArray(tls.ciphers) ? tls.ciphers : [],
136
- tlsExtensions: extensions.map((ext) => String(ext.name)),
137
- akamaiFingerprint: http2.akamai_fingerprint ?? '',
138
- akamaiFingerprintHash: http2.akamai_fingerprint_hash ?? '',
139
- headers: Array.isArray(headersFrame?.headers)
140
- ? headersFrame.headers
141
- : [],
142
- raw: data,
143
- };
144
101
  }
145
102
  async scrape(url, selectors, options) {
146
103
  this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, `Scraping ${url}`));
@@ -149,35 +106,12 @@ let BrowserActionService = BrowserActionService_1 = class BrowserActionService {
149
106
  : options?.cloak;
150
107
  const pageService = await this.newPage();
151
108
  const page = await pageService.navigateTo(url, this.buildNavOptions(options), cloak, options?.interceptResource);
152
- const result = {};
153
- const evalOne = page['$eval'].bind(page);
154
109
  try {
155
- await Promise.all(Object.entries(selectors).map(async ([key, rawSelector]) => {
156
- const { selector, attribute } = this.parseSelector(rawSelector);
157
- try {
158
- const value = attribute
159
- ? await evalOne(selector, (el, attr) => el.getAttribute(attr), attribute)
160
- : await evalOne(selector, (el) => el.textContent);
161
- result[key] = options?.pipes?.[key]
162
- ? this.pipeEngine.apply(typeof value === 'string' ? value : String(value ?? ''), options.pipes[key], url)
163
- : value;
164
- }
165
- catch (err) {
166
- this.logger.warn(`Failed to scrape '${key}' (${rawSelector}): ${err instanceof Error ? err.message : String(err)}`);
167
- }
168
- }));
110
+ return await this.extraction.extractSingle(page, url, selectors, options?.pipes);
169
111
  }
170
112
  finally {
171
113
  await pageService.closePage();
172
114
  }
173
- return result;
174
- }
175
- parseSelector(raw) {
176
- const match = raw.match(/^(.*)@([A-Za-z_][\w-]*)$/);
177
- if (match && match[1].trim() !== '') {
178
- return { selector: match[1], attribute: match[2] };
179
- }
180
- return { selector: raw };
181
115
  }
182
116
  buildNavOptions(options) {
183
117
  if (!options?.waitUntil && !options?.timeout)
@@ -196,9 +130,9 @@ let BrowserActionService = BrowserActionService_1 = class BrowserActionService {
196
130
  const page = await pageService.navigateTo(url, this.buildNavOptions(options), cloak, options?.interceptResource);
197
131
  const result = {};
198
132
  const evalAll = page['$$eval'].bind(page);
199
- Object.entries(selectors).forEach(([key, selector]) => this.validateSelector(key, selector));
133
+ Object.entries(selectors).forEach(([key, selector]) => this.extraction.validateSelector(key, selector));
200
134
  await Promise.all(Object.entries(selectors).map(async ([key, rawSelector]) => {
201
- const { selector, attribute } = this.parseSelector(rawSelector);
135
+ const { selector, attribute } = this.extraction.parseSelector(rawSelector);
202
136
  try {
203
137
  const isXPath = (0, dom_util_1.isXPathSelector)(selector);
204
138
  let values;
@@ -337,7 +271,7 @@ let BrowserActionService = BrowserActionService_1 = class BrowserActionService {
337
271
  const template = pagination.urlTemplate ?? url;
338
272
  const startPage = pagination.startPage ?? 2;
339
273
  const { items: page1Items } = await this.scrapeContainerFields(url, descriptor, scraperOpts);
340
- const { items: restItems, pages: restPages } = await this.paginateUrlIncrement(template, async (pageUrl) => {
274
+ const { items: restItems, pages: restPages } = await this.pagination.paginateUrlIncrement(template, async (pageUrl) => {
341
275
  const { items } = await this.scrapeContainerFields(pageUrl, descriptor, scraperOpts);
342
276
  return items;
343
277
  }, pagination, startPage);
@@ -383,15 +317,15 @@ let BrowserActionService = BrowserActionService_1 = class BrowserActionService {
383
317
  });
384
318
  };
385
319
  if (pagination.type === 'click-next') {
386
- const { items, pages } = await this.paginateClickNext(page, containerFn, pagination);
320
+ const { items, pages } = await this.pagination.paginateClickNext(page, containerFn, pagination);
387
321
  return { results: items, totalPages: pages };
388
322
  }
389
323
  else if (pagination.type === 'load-more') {
390
- const { items, pages } = await this.paginateLoadMore(page, containerFn, pagination);
324
+ const { items, pages } = await this.pagination.paginateLoadMore(page, containerFn, pagination);
391
325
  return { results: items, totalPages: pages };
392
326
  }
393
327
  else {
394
- const { items, pages } = await this.paginateInfiniteScroll(page, containerFn, pagination);
328
+ const { items, pages } = await this.pagination.paginateInfiniteScroll(page, containerFn, pagination);
395
329
  return { results: items, totalPages: pages };
396
330
  }
397
331
  }
@@ -399,249 +333,6 @@ let BrowserActionService = BrowserActionService_1 = class BrowserActionService {
399
333
  await pageService.closePage();
400
334
  }
401
335
  }
402
- async findPaginationElement(page, selector) {
403
- if (!(0, dom_util_1.isXPathSelector)(selector)) {
404
- return page.$(selector);
405
- }
406
- const pageWithX = page;
407
- if (typeof pageWithX.$x === 'function') {
408
- const results = await pageWithX.$x(selector);
409
- return results[0] ?? null;
410
- }
411
- const handle = await page
412
- .evaluateHandle((sel) => {
413
- const res = document.evaluate(sel, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
414
- return res.singleNodeValue;
415
- }, selector)
416
- .then((h) => h.asElement());
417
- return handle;
418
- }
419
- async paginateClickNext(page, containerFn, opts) {
420
- const max = opts.maxPages ?? 10;
421
- const wait = opts.waitAfter ?? 800;
422
- const all = [];
423
- let pages = 0;
424
- for (let i = 0; i < max; i++) {
425
- try {
426
- const items = await containerFn(page);
427
- all.push(...items);
428
- pages++;
429
- if (!opts.selector)
430
- break;
431
- const btn = await this.findPaginationElement(page, opts.selector);
432
- if (!btn)
433
- break;
434
- await Promise.all([
435
- page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
436
- btn.click(),
437
- ]);
438
- await (0, delay_util_1.delay)(wait);
439
- }
440
- catch (err) {
441
- if (isTargetClosed(err))
442
- break;
443
- if (err instanceof Error && err.message.includes('TimeoutError'))
444
- break;
445
- throw err;
446
- }
447
- }
448
- return { items: all, pages };
449
- }
450
- async paginateLoadMore(page, containerFn, opts) {
451
- const max = opts.maxPages ?? 10;
452
- const wait = opts.waitAfter ?? 800;
453
- let previousCount = 0;
454
- let clicks = 0;
455
- let lastItems = [];
456
- for (let i = 0; i < max; i++) {
457
- try {
458
- const items = await containerFn(page);
459
- if (items.length === previousCount) {
460
- return { items, pages: clicks + 1 };
461
- }
462
- lastItems = items;
463
- previousCount = items.length;
464
- if (!opts.selector)
465
- break;
466
- const btn = await this.findPaginationElement(page, opts.selector);
467
- if (!btn)
468
- break;
469
- await btn.click();
470
- clicks++;
471
- await (0, delay_util_1.delay)(wait);
472
- }
473
- catch (err) {
474
- if (isTargetClosed(err))
475
- break;
476
- throw err;
477
- }
478
- }
479
- try {
480
- const finalItems = await containerFn(page);
481
- return { items: finalItems, pages: clicks + 1 };
482
- }
483
- catch (err) {
484
- if (isTargetClosed(err))
485
- return { items: lastItems, pages: clicks + 1 };
486
- throw err;
487
- }
488
- }
489
- async paginateInfiniteScroll(page, containerFn, opts) {
490
- const max = opts.maxPages ?? 10;
491
- const wait = opts.waitAfter ?? 800;
492
- let previousCount = 0;
493
- let scrolls = 0;
494
- let lastItems = [];
495
- for (let i = 0; i < max; i++) {
496
- try {
497
- const items = await containerFn(page);
498
- if (opts.endSelector) {
499
- const endEl = await this.findPaginationElement(page, opts.endSelector);
500
- if (endEl)
501
- return { items, pages: scrolls + 1 };
502
- }
503
- if (items.length === previousCount)
504
- return { items, pages: scrolls + 1 };
505
- lastItems = items;
506
- previousCount = items.length;
507
- if (opts.selector) {
508
- const sentinel = await this.findPaginationElement(page, opts.selector);
509
- if (sentinel) {
510
- await page.evaluate((el) => el.scrollIntoView(), sentinel);
511
- }
512
- else {
513
- await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
514
- }
515
- }
516
- else {
517
- await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
518
- }
519
- scrolls++;
520
- await (0, delay_util_1.delay)(wait);
521
- }
522
- catch (err) {
523
- if (isTargetClosed(err))
524
- break;
525
- throw err;
526
- }
527
- }
528
- try {
529
- const finalItems = await containerFn(page);
530
- return { items: finalItems, pages: scrolls + 1 };
531
- }
532
- catch (err) {
533
- if (isTargetClosed(err))
534
- return { items: lastItems, pages: scrolls + 1 };
535
- throw err;
536
- }
537
- }
538
- async paginateUrlIncrement(urlTemplate, containerFn, opts, startPage) {
539
- const max = opts.maxPages ?? 10;
540
- const wait = opts.waitAfter ?? 800;
541
- const all = [];
542
- let pagesScraped = 0;
543
- for (let p = startPage; p < startPage + max; p++) {
544
- try {
545
- const url = urlTemplate.replace('{page}', String(p));
546
- const items = await containerFn(url);
547
- if (items.length === 0)
548
- break;
549
- all.push(...items);
550
- pagesScraped++;
551
- if (p < startPage + max - 1)
552
- await (0, delay_util_1.delay)(wait);
553
- }
554
- catch (err) {
555
- if (isTargetClosed(err))
556
- break;
557
- throw err;
558
- }
559
- }
560
- return { items: all, pages: pagesScraped };
561
- }
562
- async extractAllData(page, target, as = 'text', attribute) {
563
- if (target.shadowHost && !target.value) {
564
- const hosts = await page.$$(target.shadowHost);
565
- const results = await Promise.all(hosts.map((host) => host.evaluate((el, extractAs, attr) => {
566
- const sr = el.shadowRoot;
567
- if (!sr)
568
- return '';
569
- if (extractAs === 'html' || extractAs === 'outerHtml')
570
- return sr.innerHTML;
571
- if (extractAs === 'attribute')
572
- return el.getAttribute(attr || '') || '';
573
- return sr.textContent?.trim() || '';
574
- }, as, attribute)));
575
- return results;
576
- }
577
- if (target.shadowHost) {
578
- return await this.extractAllFromShadowRoot(page, target, as, attribute);
579
- }
580
- if (target.type === 'css') {
581
- return await page.$$eval(target.value, (elements, extractAs, attr) => elements.map((el) => {
582
- if (extractAs === 'html')
583
- return el.innerHTML;
584
- if (extractAs === 'outerHtml')
585
- return el.outerHTML;
586
- if (extractAs === 'attribute')
587
- return el.getAttribute(attr || '') || '';
588
- return el.textContent?.trim() || '';
589
- }), as, attribute);
590
- }
591
- return await page.evaluate((selector, extractAs, attr) => {
592
- const results = document.evaluate(selector, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
593
- const values = [];
594
- for (let i = 0; i < results.snapshotLength; i++) {
595
- const node = results.snapshotItem(i);
596
- if (!(node instanceof Element)) {
597
- values.push(node?.textContent?.trim() || '');
598
- continue;
599
- }
600
- if (extractAs === 'html')
601
- values.push(node.innerHTML);
602
- else if (extractAs === 'outerHtml')
603
- values.push(node.outerHTML);
604
- else if (extractAs === 'attribute')
605
- values.push(node.getAttribute(attr || '') || '');
606
- else
607
- values.push(node.textContent?.trim() || '');
608
- }
609
- return values;
610
- }, target.value, as, attribute);
611
- }
612
- async extractAllFromShadowRoot(page, target, as = 'text', attribute) {
613
- const hosts = await page.$$(target.shadowHost);
614
- const allResults = await Promise.all(hosts.map(async (host) => {
615
- return await host.evaluate((el, selector, targetType, extractAs, attr) => {
616
- const shadowRoot = el.shadowRoot;
617
- if (!shadowRoot)
618
- return [];
619
- const getVal = (e) => {
620
- if (extractAs === 'html')
621
- return e.innerHTML;
622
- if (extractAs === 'outerHtml')
623
- return e.outerHTML;
624
- if (extractAs === 'attribute')
625
- return e.getAttribute(attr || '') || '';
626
- return e.textContent?.trim() || '';
627
- };
628
- if (targetType === 'css') {
629
- const elements = shadowRoot.querySelectorAll(selector);
630
- return Array.from(elements).map(getVal);
631
- }
632
- const results = document.evaluate(selector, shadowRoot, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
633
- const values = [];
634
- let node;
635
- while ((node = results.iterateNext())) {
636
- values.push(node instanceof Element
637
- ? getVal(node)
638
- : node?.textContent?.trim() || '');
639
- }
640
- return values;
641
- }, target.value, target.type, as, attribute);
642
- }));
643
- return allResults.flat();
644
- }
645
336
  async waitForSelector(url, selector, timeout, scraperOptions) {
646
337
  this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, `Waiting for ${selector} on ${url}`));
647
338
  const cloak = scraperOptions?.useRandomUserAgent
@@ -685,7 +376,7 @@ let BrowserActionService = BrowserActionService_1 = class BrowserActionService {
685
376
  const errorConfig = workflow.onError || {};
686
377
  for (const action of workflow.actions) {
687
378
  try {
688
- await this.executeAction(page, action, context);
379
+ await this.workflow.executeAction(page, action, context, this.activeDebugLogMaxLength);
689
380
  }
690
381
  catch (error) {
691
382
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -722,650 +413,19 @@ let BrowserActionService = BrowserActionService_1 = class BrowserActionService {
722
413
  async scrapeAllWithWorkflow(url, workflow, variables) {
723
414
  return await this.scrapeWithWorkflow(url, workflow, variables);
724
415
  }
416
+ async executeContainerExtraction(page, descriptor, currentPage = 1) {
417
+ return this.container.executeContainerExtraction(page, descriptor, currentPage);
418
+ }
725
419
  async executeAction(page, action, context) {
726
420
  if (action.condition) {
727
421
  const shouldExecute = await this.evaluateCondition(page, action.condition);
728
- if (!shouldExecute) {
729
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, `Skipping action due to condition: ${action.action}`));
730
- return;
731
- }
732
- }
733
- const value = this.interpolateValue(String(action.value || ''), context);
734
- const actionLabel = action.id
735
- ? `[${action.action}] id="${action.id}"`
736
- : `[${action.action}]`;
737
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, `Executing action: ${actionLabel}`));
738
- const maxRetries = Math.min(action.options?.retry ?? 0, 100);
739
- const retryDelay = Math.min(action.options?.retryDelay ?? 0, 300_000);
740
- for (let attempt = 0;; attempt++) {
741
- try {
742
- await this.dispatchAction(page, action, value, context);
422
+ if (!shouldExecute)
743
423
  return;
744
- }
745
- catch (err) {
746
- if (attempt >= maxRetries)
747
- throw err;
748
- this.logger.warn((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, `Action ${actionLabel} failed (attempt ${attempt + 1}/${maxRetries + 1}), retrying: ${err instanceof Error ? err.message : String(err)}`));
749
- if (retryDelay > 0)
750
- await (0, delay_util_1.delay)(retryDelay);
751
- }
752
- }
753
- }
754
- async dispatchAction(page, action, value, context) {
755
- switch (action.action) {
756
- case 'navigate': {
757
- const navOptions = {};
758
- if (action.options?.waitUntil)
759
- navOptions.waitUntil = action.options.waitUntil;
760
- if (action.options?.timeout)
761
- navOptions.timeout = action.options.timeout;
762
- if (!(0, class_validator_1.isURL)(value, {
763
- require_protocol: true,
764
- protocols: ['http', 'https'],
765
- })) {
766
- throw new Error(`Invalid or disallowed URL: ${value}`);
767
- }
768
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` navigate → ${value}`));
769
- await page.goto(value, Object.keys(navOptions).length ? navOptions : undefined);
770
- break;
771
- }
772
- case 'wait':
773
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` wait ${Number(action.value) || 0}ms`));
774
- await (0, delay_util_1.delay)(Math.min(Number(action.value) || 0, 300_000));
775
- break;
776
- case 'waitFor':
777
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` waitFor target: ${this.describeTarget(action.target)}`));
778
- await this.waitForTarget(page, action.target, action.options);
779
- break;
780
- case 'click':
781
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` click target: ${this.describeTarget(action.target)}`));
782
- await this.clickElement(page, action.target, action.options);
783
- break;
784
- case 'type':
785
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` type target: ${this.describeTarget(action.target)} value: "${value}"`));
786
- await this.typeText(page, action.target, value, action.options);
787
- break;
788
- case 'select':
789
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` select target: ${this.describeTarget(action.target)} value: "${value}"`));
790
- await this.selectOption(page, action.target, value, action.options);
791
- break;
792
- case 'scroll':
793
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` scroll target: ${this.describeTarget(action.target)}`));
794
- await this.scrollToElement(page, action.target);
795
- break;
796
- case 'extract': {
797
- const extractOptions = action.options || {};
798
- const extractAs = extractOptions.as ?? 'text';
799
- const extractAttr = extractOptions.attribute;
800
- if (!action.target) {
801
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` extract full page HTML`));
802
- if (action.id)
803
- context[action.id] = await page.content();
804
- break;
805
- }
806
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` extract target: ${this.describeTarget(action.target)} as="${extractAs}"${extractAttr ? ` attr="${extractAttr}"` : ''}${extractOptions.multiple ? ' multiple=true' : ''}`));
807
- if (extractOptions.multiple) {
808
- const allValues = await this.extractAllData(page, action.target, extractAs, extractAttr);
809
- if (action.id) {
810
- context[action.id] = allValues;
811
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` extracted ${Array.isArray(allValues) ? allValues.length : 1} item(s) → id="${action.id}"`));
812
- }
813
- }
814
- else {
815
- const extractedValue = await this.extractData(page, action.target, extractAs, extractAttr);
816
- if (action.id) {
817
- context[action.id] = extractedValue;
818
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` extracted → id="${action.id}": ${JSON.stringify(extractedValue)}`));
819
- }
820
- }
821
- break;
822
- }
823
- case 'screenshot': {
824
- const screenshotPath = String(action.value || `${browser_action_constants_1.DEFAULT_SCREENSHOT_FILENAME}-${Date.now()}.png`);
825
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` screenshot → ${screenshotPath}`));
826
- await page.screenshot({ path: screenshotPath });
827
- break;
828
- }
829
- case 'evaluate': {
830
- let evalCode;
831
- if (value.includes('=>')) {
832
- evalCode = `(${value})()`;
833
- }
834
- else {
835
- evalCode = value;
836
- }
837
- if (evalCode.length > 50_000) {
838
- throw new Error('evaluate script exceeds maximum length of 50000 characters');
839
- }
840
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, `Evaluating: ${evalCode}`));
841
- const evalResult = await page.evaluate(evalCode);
842
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, `Result: ${JSON.stringify(evalResult)}`));
843
- if (action.id) {
844
- context[action.id] = evalResult;
845
- }
846
- break;
847
- }
848
- case 'cleanse': {
849
- const pipes = action.options?.pipes;
850
- if (!pipes) {
851
- throw new Error('cleanse action requires pipes');
852
- }
853
- const valueKey = String(action.value || '');
854
- const rawValue = this.resolveRawValue(valueKey, context);
855
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` cleanse raw: ${JSON.stringify(rawValue)}`));
856
- const cleanedValue = Array.isArray(rawValue)
857
- ? rawValue.map((item) => this.pipeEngine.apply(String(item), pipes))
858
- : this.pipeEngine.apply(String(rawValue), pipes);
859
- if (action.id) {
860
- context[action.id] = cleanedValue;
861
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, `Cleansed value for '${action.id}': ${JSON.stringify(cleanedValue)}`));
862
- }
863
- break;
864
- }
865
- case 'saveCookies': {
866
- const sessionName = String(value);
867
- const overwrite = action.options?.overwrite ?? false;
868
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` saveCookies session="${sessionName}" overwrite=${overwrite}`));
869
- const metadata = {};
870
- if (action.options?.metadata) {
871
- Object.assign(metadata, action.options.metadata);
872
- }
873
- await this.cookieService.saveCookies(page, sessionName, {
874
- overwrite,
875
- metadata,
876
- });
877
- break;
878
- }
879
- case 'loadCookies': {
880
- const sessionName = String(value);
881
- const throwIfNotExists = action.onError !== 'skip' && action.onError !== 'continue';
882
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` loadCookies session="${sessionName}"`));
883
- await this.cookieService.loadCookies(page, sessionName, {
884
- throwIfNotExists,
885
- });
886
- break;
887
- }
888
- case 'clearCookies': {
889
- const sessionName = String(value);
890
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` clearCookies session="${sessionName}"`));
891
- await this.cookieService.deleteCookies(sessionName);
892
- break;
893
- }
894
- case 'listCookies': {
895
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` listCookies`));
896
- const sessions = await this.cookieService.listCookies();
897
- if (action.id) {
898
- context[action.id] = sessions;
899
- }
900
- break;
901
- }
902
- case 'hover':
903
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` hover target: ${this.describeTarget(action.target)}`));
904
- await this.hoverElement(page, action.target, action.options);
905
- break;
906
- case 'keyPress':
907
- if (!value) {
908
- throw new Error('keyPress action requires a key value');
909
- }
910
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` keyPress key="${value}"`));
911
- await page.keyboard.press(value);
912
- break;
913
- case 'clear':
914
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` clear target: ${this.describeTarget(action.target)}`));
915
- await this.clearElement(page, action.target);
916
- break;
917
- case 'waitForNetwork':
918
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` waitForNetwork timeout=${action.options?.timeout ?? browser_action_constants_1.DEFAULT_ACTION_TIMEOUT}ms`));
919
- await page.waitForNetworkIdle({
920
- timeout: action.options?.timeout ?? browser_action_constants_1.DEFAULT_ACTION_TIMEOUT,
921
- });
922
- break;
923
- case 'reload':
924
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` reload waitUntil="${action.options?.waitUntil ?? 'load'}"`));
925
- await page.reload({
926
- waitUntil: action.options?.waitUntil ?? 'load',
927
- timeout: action.options?.timeout ?? browser_action_constants_1.DEFAULT_NAVIGATION_TIMEOUT,
928
- });
929
- break;
930
- case 'scrapeContainer': {
931
- const o = action.options;
932
- if (!o?.container || !o?.fields) {
933
- throw new Error('scrapeContainer requires options.container and options.fields');
934
- }
935
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` scrapeContainer container="${o.container}"`));
936
- const containerResult = await this.executeContainerExtraction(page, {
937
- container: o.container,
938
- fields: o.fields,
939
- pagination: o.pagination,
940
- }, o.currentPage ?? 1);
941
- if (action.id) {
942
- context[action.id] = containerResult.items;
943
- if (containerResult.pagination) {
944
- context[`${action.id}_pagination`] = containerResult.pagination;
945
- }
946
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` scrapeContainer → id="${action.id}": ${containerResult.items.length} items`));
947
- }
948
- break;
949
- }
950
- case 'extractPagination': {
951
- const o = action.options;
952
- if (!o?.container || !o?.linkSelector || !o?.labelSelector) {
953
- throw new Error('extractPagination requires options.container, linkSelector, and labelSelector');
954
- }
955
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` extractPagination container="${o.container}"`));
956
- const paginationResult = await this.resolvePagination(page, {
957
- container: o.container,
958
- linkSelector: o.linkSelector,
959
- labelSelector: o.labelSelector,
960
- }, o.currentPage ?? 1);
961
- if (action.id) {
962
- context[action.id] = paginationResult;
963
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` extractPagination → id="${action.id}": nextUrl=${paginationResult.nextUrl}`));
964
- }
965
- break;
966
- }
967
- default: {
968
- const exhaustiveCheck = action.action;
969
- throw new Error(`Unknown action type: ${String(exhaustiveCheck)}`);
970
- }
971
424
  }
425
+ await this.workflow.executeAction(page, { ...action, condition: undefined }, context, this.activeDebugLogMaxLength);
972
426
  }
973
427
  async evaluateCondition(page, condition) {
974
- if (!condition)
975
- return true;
976
- if (condition.ifExists) {
977
- const element = await this.findElement(page, condition.ifExists);
978
- return element !== null;
979
- }
980
- if (condition.unlessExists) {
981
- const element = await this.findElement(page, condition.unlessExists);
982
- return element === null;
983
- }
984
- return true;
985
- }
986
- describeTarget(target) {
987
- const base = target.value
988
- ? `${target.type}="${target.value}"`
989
- : `(no selector)`;
990
- return target.shadowHost
991
- ? `shadowHost="${target.shadowHost}" > ${base}`
992
- : base;
993
- }
994
- async findElement(page, target) {
995
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` findElement: ${this.describeTarget(target)}`));
996
- if (target.shadowHost) {
997
- const el = await this.findElementInShadowRoot(page, target);
998
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` findElement result: ${el ? 'found' : 'NOT FOUND'}`));
999
- return el;
1000
- }
1001
- if (target.type === 'css') {
1002
- const el = await page.$(target.value);
1003
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` findElement result: ${el ? 'found' : 'NOT FOUND'}`));
1004
- return el;
1005
- }
1006
- const el = await page
1007
- .evaluateHandle((selector) => {
1008
- const result = document.evaluate(selector, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
1009
- return result.singleNodeValue;
1010
- }, target.value)
1011
- .then((handle) => handle.asElement());
1012
- this.logger.debug((0, truncate_log_util_1.truncateLog)(this.activeDebugLogMaxLength, ` findElement result: ${el ? 'found' : 'NOT FOUND'}`));
1013
- return el;
1014
- }
1015
- async findElementInShadowRoot(page, target) {
1016
- const host = await page.$(target.shadowHost);
1017
- if (!host)
1018
- return null;
1019
- return host
1020
- .evaluateHandle((el, selector, targetType) => {
1021
- const shadowRoot = el.shadowRoot;
1022
- if (!shadowRoot)
1023
- return null;
1024
- if (targetType === 'css') {
1025
- return shadowRoot.querySelector(selector);
1026
- }
1027
- const result = document.evaluate(selector, shadowRoot, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
1028
- return result.singleNodeValue;
1029
- }, target.value, target.type)
1030
- .then((handle) => handle.asElement());
1031
- }
1032
- async waitForTarget(page, target, options) {
1033
- const timeout = options?.timeout || browser_action_constants_1.DEFAULT_ACTION_TIMEOUT;
1034
- if (target.type === 'css') {
1035
- await page.waitForSelector(target.value, { timeout });
1036
- }
1037
- else {
1038
- await page.waitForFunction((selector) => {
1039
- const result = document.evaluate(selector, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
1040
- return result.singleNodeValue !== null;
1041
- }, { timeout }, target.value);
1042
- }
1043
- }
1044
- async clickElement(page, target, options) {
1045
- const element = await this.findElement(page, target);
1046
- if (!element) {
1047
- throw new Error(`Element not found: ${target.value}`);
1048
- }
1049
- await this.maybeScrollToElement(element, options?.scrollTo);
1050
- const elem = element;
1051
- await elem.click();
1052
- if (options?.waitForNavigation) {
1053
- await page.waitForNavigation({
1054
- timeout: options.navigationTimeout || browser_action_constants_1.DEFAULT_NAVIGATION_TIMEOUT,
1055
- });
1056
- }
1057
- }
1058
- async typeText(page, target, value, options) {
1059
- const element = await this.findElement(page, target);
1060
- if (!element) {
1061
- throw new Error(`Element not found: ${target.value}`);
1062
- }
1063
- await this.maybeScrollToElement(element, options?.scrollTo);
1064
- const elem = element;
1065
- await elem.click();
1066
- const delay = options?.delay || 0;
1067
- await elem.type(value, { delay });
1068
- await elem.evaluate((el) => {
1069
- el.dispatchEvent(new Event('change', { bubbles: true }));
1070
- el.dispatchEvent(new Event('blur', { bubbles: true }));
1071
- });
1072
- }
1073
- async selectOption(page, target, value, options) {
1074
- const element = await this.findElement(page, target);
1075
- if (!element) {
1076
- throw new Error(`Element not found: ${target.value}`);
1077
- }
1078
- await this.maybeScrollToElement(element, options?.scrollTo);
1079
- const elem = element;
1080
- await elem.select(value);
1081
- }
1082
- async scrollToElement(page, target) {
1083
- const element = await this.findElement(page, target);
1084
- if (!element) {
1085
- throw new Error(`Element not found: ${target.value}`);
1086
- }
1087
- await this.scrollElementIntoView(element);
1088
- }
1089
- async maybeScrollToElement(element, shouldScroll) {
1090
- if (shouldScroll) {
1091
- await this.scrollElementIntoView(element);
1092
- }
1093
- }
1094
- async scrollElementIntoView(element) {
1095
- await element.evaluate((el) => el.scrollIntoView({ behavior: 'smooth', block: 'center' }), {});
1096
- await (0, delay_util_1.delay)(browser_action_constants_1.DEFAULT_SCROLL_DELAY_MS);
1097
- }
1098
- async executeContainerExtraction(page, descriptor, currentPage = 1) {
1099
- const rawItems = await page.evaluate((containerSel, fieldDefs) => {
1100
- const isXPath = (s) => s.trim().startsWith('//') ||
1101
- s.trim().startsWith('(') ||
1102
- s.trim().startsWith('./');
1103
- const getContainerNodes = (sel) => {
1104
- if (isXPath(sel)) {
1105
- const res = document.evaluate(sel, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
1106
- const nodes = [];
1107
- for (let i = 0; i < res.snapshotLength; i++)
1108
- nodes.push(res.snapshotItem(i));
1109
- return nodes;
1110
- }
1111
- return Array.from(document.querySelectorAll(sel));
1112
- };
1113
- const extractOne = (sel, root, attr, returnType) => {
1114
- let node = null;
1115
- if (isXPath(sel)) {
1116
- const res = document.evaluate(sel, root, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
1117
- node = res.singleNodeValue;
1118
- }
1119
- else {
1120
- node = root.querySelector(sel);
1121
- }
1122
- if (!node)
1123
- return '';
1124
- if (attr)
1125
- return node.getAttribute?.(attr) ?? '';
1126
- if (returnType === 'html')
1127
- return node.innerHTML ?? '';
1128
- return node.textContent?.trim() ?? '';
1129
- };
1130
- const extractMany = (sel, root, attr, returnType) => {
1131
- let nodes = [];
1132
- if (isXPath(sel)) {
1133
- const res = document.evaluate(sel, root, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
1134
- for (let i = 0; i < res.snapshotLength; i++)
1135
- nodes.push(res.snapshotItem(i));
1136
- }
1137
- else {
1138
- nodes = Array.from(root.querySelectorAll(sel));
1139
- }
1140
- return nodes.map((n) => {
1141
- if (attr)
1142
- return n.getAttribute?.(attr) ?? '';
1143
- if (returnType === 'html')
1144
- return n.innerHTML ?? '';
1145
- return n.textContent?.trim() ?? '';
1146
- });
1147
- };
1148
- const extractField = (root, fd) => {
1149
- const selectors = [fd.selector, ...(fd.fallback ?? [])];
1150
- for (const sel of selectors) {
1151
- const val = fd.multiple
1152
- ? extractMany(sel, root, fd.attribute, fd.returnType)
1153
- : extractOne(sel, root, fd.attribute, fd.returnType);
1154
- if (Array.isArray(val) ? val.length > 0 : val !== '')
1155
- return val;
1156
- }
1157
- return fd.multiple ? [] : '';
1158
- };
1159
- return getContainerNodes(containerSel).map((node) => {
1160
- const item = {};
1161
- for (const [key, fd] of Object.entries(fieldDefs))
1162
- item[key] = extractField(node, fd);
1163
- return item;
1164
- });
1165
- }, descriptor.container, descriptor.fields);
1166
- const items = rawItems;
1167
- let pagination;
1168
- if (descriptor.pagination) {
1169
- pagination = await this.resolvePagination(page, descriptor.pagination, currentPage);
1170
- }
1171
- return { items, pagination };
1172
- }
1173
- async resolvePagination(page, descriptor, currentPage = 1) {
1174
- currentPage = Math.max(1, currentPage);
1175
- const rawPages = await page.evaluate((containerSel, linkSel, labelSel) => {
1176
- const getNode = (sel, root) => {
1177
- if (sel.trim().startsWith('//') ||
1178
- sel.trim().startsWith('(') ||
1179
- sel.trim().startsWith('./')) {
1180
- const res = document.evaluate(sel, root, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
1181
- return res.singleNodeValue;
1182
- }
1183
- return root.querySelector(sel);
1184
- };
1185
- const getNodes = (sel, root) => {
1186
- if (sel.trim().startsWith('//') ||
1187
- sel.trim().startsWith('(') ||
1188
- sel.trim().startsWith('./')) {
1189
- const res = document.evaluate(sel, root, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
1190
- const nodes = [];
1191
- for (let i = 0; i < res.snapshotLength; i++)
1192
- nodes.push(res.snapshotItem(i));
1193
- return nodes;
1194
- }
1195
- return Array.from(root.querySelectorAll(sel));
1196
- };
1197
- const container = getNode(containerSel, document);
1198
- if (!container)
1199
- return [];
1200
- return getNodes(linkSel, container).map((el) => {
1201
- const isXPath = (s) => s.trim().startsWith('//') ||
1202
- s.trim().startsWith('(') ||
1203
- s.trim().startsWith('./');
1204
- const labelNodes = isXPath(labelSel)
1205
- ? (() => {
1206
- const r = document.evaluate(labelSel, el, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
1207
- const ns = [];
1208
- for (let i = 0; i < r.snapshotLength; i++)
1209
- ns.push(r.snapshotItem(i));
1210
- return ns;
1211
- })()
1212
- : Array.from(el.querySelectorAll(labelSel));
1213
- const label = labelNodes.length > 0
1214
- ? (labelNodes[0].textContent?.trim() ?? '')
1215
- : (el.textContent?.trim() ?? '');
1216
- return {
1217
- label,
1218
- url: el.getAttribute('href') ?? '',
1219
- };
1220
- });
1221
- }, descriptor.container, descriptor.linkSelector, descriptor.labelSelector);
1222
- const pages = rawPages.filter((p) => p.url);
1223
- const numericNext = pages
1224
- .filter((p) => !Number.isNaN(parseInt(p.label, 10)) &&
1225
- parseInt(p.label, 10) > currentPage)
1226
- .sort((a, b) => parseInt(a.label, 10) - parseInt(b.label, 10))[0];
1227
- const nextUrl = numericNext?.url ??
1228
- pages.find((p) => ['next', '>'].includes(p.label.toLowerCase()))?.url ??
1229
- null;
1230
- return { pages, nextUrl };
1231
- }
1232
- validateSelector(key, selector) {
1233
- if (!selector || selector.trim() === '') {
1234
- throw new Error(`Selector for '${key}' cannot be empty`);
1235
- }
1236
- }
1237
- async extractData(page, target, as = 'text', attribute) {
1238
- if (target.shadowHost && !target.value) {
1239
- const host = await page.$(target.shadowHost);
1240
- if (!host)
1241
- throw new Error(`Shadow host not found: ${target.shadowHost}`);
1242
- return host.evaluate((el, extractAs, attr) => {
1243
- const sr = el.shadowRoot;
1244
- if (!sr)
1245
- return '';
1246
- if (extractAs === 'html' || extractAs === 'outerHtml')
1247
- return sr.innerHTML;
1248
- if (extractAs === 'attribute')
1249
- return el.getAttribute(attr || '') || '';
1250
- return sr.textContent?.trim() || '';
1251
- }, as, attribute);
1252
- }
1253
- const element = await this.findElement(page, target);
1254
- if (!element) {
1255
- throw new Error(`Element not found: ${target.value}`);
1256
- }
1257
- return element.evaluate((el, extractAs, attr) => {
1258
- if (extractAs === 'html')
1259
- return el.innerHTML;
1260
- if (extractAs === 'outerHtml')
1261
- return el.outerHTML;
1262
- if (extractAs === 'attribute')
1263
- return el.getAttribute(attr || '') || '';
1264
- return el.textContent?.trim() || '';
1265
- }, as, attribute);
1266
- }
1267
- async hoverElement(page, target, options) {
1268
- const element = await this.findElement(page, target);
1269
- if (!element) {
1270
- throw new Error(`Element not found: ${target.value}`);
1271
- }
1272
- await this.maybeScrollToElement(element, options?.scrollTo);
1273
- const elem = element;
1274
- await elem.hover();
1275
- }
1276
- async clearElement(page, target) {
1277
- const element = await this.findElement(page, target);
1278
- if (!element) {
1279
- throw new Error(`Element not found: ${target.value}`);
1280
- }
1281
- await element.evaluate((el) => {
1282
- if ('value' in el) {
1283
- el.value = '';
1284
- el.dispatchEvent(new Event('input', { bubbles: true }));
1285
- el.dispatchEvent(new Event('change', { bubbles: true }));
1286
- }
1287
- });
1288
- }
1289
- resolveRawValue(value, context) {
1290
- if (value.startsWith('${') && value.endsWith('}')) {
1291
- const path = value.slice(2, -1);
1292
- if (!path.includes('${') && !path.includes('.') && !path.includes('[')) {
1293
- return context[path] ?? value;
1294
- }
1295
- }
1296
- return this.resolveValue(value, context);
1297
- }
1298
- resolveValue(value, context) {
1299
- if (value.startsWith('${') && value.endsWith('}')) {
1300
- const path = value.slice(2, -1);
1301
- if (!path.includes('${')) {
1302
- const keys = path.split('.');
1303
- let result = context;
1304
- for (const key of keys) {
1305
- const arrayMatch = key.match(/^(\w+)\[(\d+)\]$/);
1306
- if (arrayMatch) {
1307
- const [, arrayKey, indexStr] = arrayMatch;
1308
- if (!result || typeof result !== 'object') {
1309
- return value;
1310
- }
1311
- const arrayValue = result[arrayKey];
1312
- const index = parseInt(indexStr, 10);
1313
- result = Array.isArray(arrayValue) ? arrayValue[index] : undefined;
1314
- }
1315
- else {
1316
- if (!result || typeof result !== 'object') {
1317
- return value;
1318
- }
1319
- result = result[key];
1320
- }
1321
- if (result === undefined) {
1322
- return value;
1323
- }
1324
- }
1325
- if (result === null || typeof result === 'object') {
1326
- return value;
1327
- }
1328
- return result;
1329
- }
1330
- return this.interpolateValue(path, context);
1331
- }
1332
- return value;
1333
- }
1334
- interpolateValue(value, context) {
1335
- return value.replace(/\$\{([^}]+)\}/g, (_match, path) => {
1336
- const keys = path.split('.');
1337
- let result = context;
1338
- for (const key of keys) {
1339
- if (key === '__proto__' ||
1340
- key === 'constructor' ||
1341
- key === 'prototype') {
1342
- return '';
1343
- }
1344
- const arrayMatch = key.match(/^(\w+)\[(\d+)\]$/);
1345
- if (arrayMatch) {
1346
- const [, arrayKey, indexStr] = arrayMatch;
1347
- if (!result || typeof result !== 'object') {
1348
- return '';
1349
- }
1350
- const arrayValue = result[arrayKey];
1351
- const index = parseInt(indexStr, 10);
1352
- result = Array.isArray(arrayValue) ? arrayValue[index] : undefined;
1353
- }
1354
- else {
1355
- if (!result || typeof result !== 'object') {
1356
- return '';
1357
- }
1358
- result = result[key];
1359
- }
1360
- if (result === undefined) {
1361
- return '';
1362
- }
1363
- }
1364
- if (result === null || typeof result === 'object') {
1365
- return '';
1366
- }
1367
- return result;
1368
- });
428
+ return this.workflow.evaluateCondition(page, condition, this.activeDebugLogMaxLength);
1369
429
  }
1370
430
  };
1371
431
  exports.BrowserActionService = BrowserActionService;