@crawlee/playwright 4.0.0-beta.1 → 4.0.0-beta.100

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 (31) hide show
  1. package/README.md +17 -13
  2. package/index.d.ts +1 -2
  3. package/index.js +0 -1
  4. package/internals/adaptive-playwright-crawler.d.ts +117 -63
  5. package/internals/adaptive-playwright-crawler.js +359 -214
  6. package/internals/enqueue-links/click-elements.d.ts +37 -55
  7. package/internals/enqueue-links/click-elements.js +51 -43
  8. package/internals/playwright-crawler.d.ts +107 -86
  9. package/internals/playwright-crawler.js +87 -43
  10. package/internals/playwright-launcher.d.ts +6 -5
  11. package/internals/playwright-launcher.js +10 -11
  12. package/internals/utils/playwright-utils.d.ts +62 -27
  13. package/internals/utils/playwright-utils.js +99 -86
  14. package/internals/utils/rendering-type-prediction.d.ts +27 -12
  15. package/internals/utils/rendering-type-prediction.js +67 -26
  16. package/package.json +17 -12
  17. package/index.d.ts.map +0 -1
  18. package/index.js.map +0 -1
  19. package/internals/adaptive-playwright-crawler.d.ts.map +0 -1
  20. package/internals/adaptive-playwright-crawler.js.map +0 -1
  21. package/internals/enqueue-links/click-elements.d.ts.map +0 -1
  22. package/internals/enqueue-links/click-elements.js.map +0 -1
  23. package/internals/playwright-crawler.d.ts.map +0 -1
  24. package/internals/playwright-crawler.js.map +0 -1
  25. package/internals/playwright-launcher.d.ts.map +0 -1
  26. package/internals/playwright-launcher.js.map +0 -1
  27. package/internals/utils/playwright-utils.d.ts.map +0 -1
  28. package/internals/utils/playwright-utils.js.map +0 -1
  29. package/internals/utils/rendering-type-prediction.d.ts.map +0 -1
  30. package/internals/utils/rendering-type-prediction.js.map +0 -1
  31. package/tsconfig.build.tsbuildinfo +0 -1
@@ -17,14 +17,13 @@
17
17
  * ```
18
18
  * @module playwrightUtils
19
19
  */
20
- import { Configuration, type Request, type Session } from '@crawlee/browser';
21
- import type { BatchAddRequestsResult } from '@crawlee/types';
22
- import { type CheerioRoot, type Dictionary } from '@crawlee/utils';
20
+ import { Configuration, type Request } from '@crawlee/browser';
21
+ import type { BatchAddRequestsResult, Dictionary } from '@crawlee/types';
22
+ import { type CheerioRoot } from '@crawlee/utils';
23
23
  // @ts-ignore optional peer dependency or compatibility with es2022
24
- import type { Page, Response } from 'playwright';
24
+ import type { Download, Page, Response } from 'playwright';
25
25
  import type { EnqueueLinksByClickingElementsOptions } from '../enqueue-links/click-elements.js';
26
26
  import { enqueueLinksByClickingElements } from '../enqueue-links/click-elements.js';
27
- import type { PlaywrightCrawlerOptions, PlaywrightCrawlingContext } from '../playwright-crawler.js';
28
27
  import { RenderingTypePredictor } from './rendering-type-prediction.js';
29
28
  export interface InjectFileOptions {
30
29
  /**
@@ -266,9 +265,9 @@ export interface SaveSnapshotOptions {
266
265
  keyValueStoreName?: string | null;
267
266
  /**
268
267
  * Configuration of the crawler that will be used to save the snapshot.
269
- * @default Configuration.getGlobalConfig()
268
+ * @default Configuration.getGlobalConfiguration()
270
269
  */
271
- config?: Configuration;
270
+ configuration?: Configuration;
272
271
  }
273
272
  /**
274
273
  * Saves a full screenshot and HTML of the current page into a Key-Value store.
@@ -290,7 +289,7 @@ export declare function saveSnapshot(page: Page, options?: SaveSnapshotOptions):
290
289
  */
291
290
  export declare function parseWithCheerio(page: Page, ignoreShadowRoots?: boolean, ignoreIframes?: boolean): Promise<CheerioRoot>;
292
291
  export declare function closeCookieModals(page: Page): Promise<void>;
293
- interface HandleCloudflareChallengeOptions {
292
+ export interface HandleCloudflareChallengeOptions {
294
293
  /** Logging defaults to the `debug` level, use this flag to log to `info` level instead. */
295
294
  verbose?: boolean;
296
295
  /** How long should we wait after the challenge is completed for the final page to load. */
@@ -304,6 +303,13 @@ interface HandleCloudflareChallengeOptions {
304
303
  isChallengeCallback?: (page: Page) => Promise<boolean>;
305
304
  /** Allows overriding the detection of Cloudflare "blocked page". */
306
305
  isBlockedCallback?: (page: Page) => Promise<boolean>;
306
+ /** Allows overriding how the checkbox click position is calculated. */
307
+ clickPositionCallback?: (page: Page) => Promise<{
308
+ x: number;
309
+ y: number;
310
+ } | null>;
311
+ /** Optional delay (in seconds) before the first click attempt on the challenge checkbox. Defaults to 1s. */
312
+ preChallengeSleepSecs?: number;
307
313
  }
308
314
  /**
309
315
  * This helper tries to solve the Cloudflare challenge automatically by clicking on the checkbox.
@@ -312,24 +318,24 @@ interface HandleCloudflareChallengeOptions {
312
318
  * result in a SessionError which will be automatically retried, so only successful requests will get
313
319
  * into the `requestHandler`.
314
320
  *
321
+ * On a successfully solved challenge the page is reloaded and the new {@link Response} is returned, so
322
+ * it can be propagated back to the crawling context via a hook return value (see
323
+ * {@link handleCloudflareChallengeHook}).
324
+ *
315
325
  * Works best with camoufox.
316
326
  *
317
327
  * **Example usage**
318
328
  * ```ts
319
329
  * postNavigationHooks: [
320
- * async ({ handleCloudflareChallenge }) => {
321
- * await handleCloudflareChallenge();
322
- * },
330
+ * async (context) => ({ response: await context.handleCloudflareChallenge() }),
323
331
  * ],
324
332
  * ```
325
333
  *
326
334
  * @param page Playwright [`Page`](https://playwright.dev/docs/api/class-page) object
327
335
  * @param url current URL for request identification, only used for logging
328
- * @param [session] current session object
329
336
  * @param [options]
330
337
  */
331
- declare function handleCloudflareChallenge(page: Page, url: string, session?: Session, options?: HandleCloudflareChallengeOptions): Promise<void>;
332
- /** @internal */
338
+ declare function handleCloudflareChallenge(page: Page, url: string, options?: HandleCloudflareChallengeOptions): Promise<Response | undefined>;
333
339
  export interface PlaywrightContextUtils {
334
340
  /**
335
341
  * Injects a JavaScript file into current `page`.
@@ -448,8 +454,7 @@ export interface PlaywrightContextUtils {
448
454
  * in `href` elements, but rather navigations are triggered in click handlers.
449
455
  * If you're looking to find URLs in `href` attributes of the page, see {@link enqueueLinks}.
450
456
  *
451
- * Optionally, the function allows you to filter the target links' URLs using an array of {@link PseudoUrl} objects
452
- * and override settings of the enqueued {@link Request} objects.
457
+ * Optionally, the function allows you to filter the target links' URLs using an array of glob or regexp patterns.
453
458
  *
454
459
  * **IMPORTANT**: To be able to do this, this function uses various mutations on the page,
455
460
  * such as changing the Z-index of elements being clicked and their visibility. Therefore,
@@ -470,9 +475,9 @@ export interface PlaywrightContextUtils {
470
475
  * async requestHandler({ enqueueLinksByClickingElements }) {
471
476
  * await enqueueLinksByClickingElements({
472
477
  * selector: 'a.product-detail',
473
- * globs: [
474
- * 'https://www.example.com/handbags/**'
475
- * 'https://www.example.com/purses/**'
478
+ * include: [
479
+ * 'https://www.example.com/handbags/**',
480
+ * 'https://www.example.com/purses/**',
476
481
  * ],
477
482
  * });
478
483
  * });
@@ -480,7 +485,7 @@ export interface PlaywrightContextUtils {
480
485
  *
481
486
  * @returns Promise that resolves to {@link BatchAddRequestsResult} object.
482
487
  */
483
- enqueueLinksByClickingElements(options: Omit<EnqueueLinksByClickingElementsOptions, 'page' | 'requestQueue'>): Promise<BatchAddRequestsResult>;
488
+ enqueueLinksByClickingElements(options: Omit<EnqueueLinksByClickingElementsOptions, 'page' | 'requestManager'>): Promise<BatchAddRequestsResult>;
484
489
  /**
485
490
  * Compiles a Playwright script into an async function that may be executed at any time
486
491
  * by providing it with the following object:
@@ -510,6 +515,15 @@ export interface PlaywrightContextUtils {
510
515
  compileScript(scriptString: string, ctx?: Dictionary): CompiledScriptFunction;
511
516
  /**
512
517
  * Tries to close cookie consent modals on the page. Based on the I Don't Care About Cookies browser extension.
518
+ *
519
+ * Note that this method requires the idcac-playwright package to be installed.
520
+ * Crawlee does not include it by default due to licensing issues.
521
+ *
522
+ * To use this method, please install the package manually by running:
523
+ *
524
+ * ```bash
525
+ * npm install idcac-playwright
526
+ * ```
513
527
  */
514
528
  closeCookieModals(): Promise<void>;
515
529
  /**
@@ -519,22 +533,44 @@ export interface PlaywrightContextUtils {
519
533
  * result in a SessionError which will be automatically retried, so only successful requests will get
520
534
  * into the `requestHandler`.
521
535
  *
522
- * Works best with camoufox.
536
+ * On a successfully solved challenge the page is reloaded and the new {@link Response} is returned,
537
+ * which can be returned from the hook to update the crawling context's `response`. For the common case,
538
+ * prefer the pre-wrapped {@link handleCloudflareChallengeHook} hook.
523
539
  *
524
540
  * **Example usage**
525
541
  * ```ts
526
542
  * postNavigationHooks: [
527
- * async ({ handleCloudflareChallenge }) => {
528
- * await handleCloudflareChallenge();
529
- * },
543
+ * async (context) => ({ response: await context.handleCloudflareChallenge() }),
530
544
  * ],
531
545
  * ```
532
546
  *
533
547
  * @param [options]
534
548
  */
535
- handleCloudflareChallenge(options?: HandleCloudflareChallengeOptions): Promise<void>;
549
+ handleCloudflareChallenge(options?: HandleCloudflareChallengeOptions): Promise<Response | undefined>;
550
+ /**
551
+ * Returns the list of {@link https://playwright.dev/docs/api/class-download | Download} objects
552
+ * collected during the current page navigation and request handler.
553
+ *
554
+ * Useful for accessing files that the page downloads automatically.
555
+ * For most use cases, prefer re-enqueueing the URL to {@link FileDownload}.
556
+ * Use this only when direct access to the Playwright `Download` object is required.
557
+ *
558
+ * **Example usage**
559
+ * ```ts
560
+ * requestHandler: async ({ listDownloads }) => {
561
+ * for (const download of await listDownloads()) {
562
+ * try {
563
+ * const stream = await download.createReadStream();
564
+ * // stream to storage...
565
+ * } catch {
566
+ * // download failed or was cancelled
567
+ * }
568
+ * }
569
+ * },
570
+ * ```
571
+ */
572
+ listDownloads(): Promise<Download[]>;
536
573
  }
537
- export declare function registerUtilsToContext(context: PlaywrightCrawlingContext, crawlerOptions: PlaywrightCrawlerOptions): void;
538
574
  export { enqueueLinksByClickingElements };
539
575
  /** @internal */
540
576
  export declare const playwrightUtils: {
@@ -551,4 +587,3 @@ export declare const playwrightUtils: {
551
587
  RenderingTypePredictor: typeof RenderingTypePredictor;
552
588
  handleCloudflareChallenge: typeof handleCloudflareChallenge;
553
589
  };
554
- //# sourceMappingURL=playwright-utils.d.ts.map
@@ -20,16 +20,14 @@
20
20
  import { readFile } from 'node:fs/promises';
21
21
  import { createRequire } from 'node:module';
22
22
  import vm from 'node:vm';
23
- import { Configuration, KeyValueStore, RequestState, SessionError, validators, } from '@crawlee/browser';
23
+ import { Configuration, KeyValueStore, serviceLocator, SessionError, validators } from '@crawlee/browser';
24
24
  import { expandShadowRoots, sleep } from '@crawlee/utils';
25
25
  import * as cheerio from 'cheerio';
26
- import { getInjectableScript as getCookieClosingScript } from 'idcac-playwright';
27
26
  import ow from 'ow';
28
27
  import { LruCache } from '@apify/datastructures';
29
- import log_ from '@apify/log';
30
28
  import { enqueueLinksByClickingElements } from '../enqueue-links/click-elements.js';
31
29
  import { RenderingTypePredictor } from './rendering-type-prediction.js';
32
- const log = log_.child({ prefix: 'Playwright Utils' });
30
+ const getLog = () => serviceLocator.getChildLog('Playwright Utils');
33
31
  const require = createRequire(import.meta.url);
34
32
  const jqueryPath = require.resolve('jquery');
35
33
  const MAX_INJECT_FILE_CACHE_SIZE = 10;
@@ -64,7 +62,7 @@ export async function injectFile(page, filePath, options = {}) {
64
62
  if (options.surviveNavigations) {
65
63
  page.on('framenavigated', async () => page
66
64
  .evaluate(contents)
67
- .catch((error) => log.warning('An error occurred during the script injection!', { error })));
65
+ .catch((error) => getLog().warning('An error occurred during the script injection!', { error })));
68
66
  }
69
67
  return evalP;
70
68
  }
@@ -121,9 +119,9 @@ export async function gotoExtended(page, request, gotoOptions = {}) {
121
119
  ow(gotoOptions, ow.object);
122
120
  const { url, method, headers, payload } = request;
123
121
  const isEmpty = (o) => !o || Object.keys(o).length === 0;
124
- if (method !== 'GET' || payload || !isEmpty(headers)) {
122
+ if (method !== 'GET' || payload) {
125
123
  // This is not deprecated, we use it to log only once.
126
- log.deprecated('Using other request methods than GET, rewriting headers and adding payloads has a high impact on performance ' +
124
+ getLog().deprecated('Using other request methods than GET, rewriting headers and adding payloads has a high impact on performance ' +
127
125
  'in recent versions of Playwright. Use only when necessary.');
128
126
  let wasCalled = false;
129
127
  const interceptRequestHandler = async (route) => {
@@ -144,12 +142,15 @@ export async function gotoExtended(page, request, gotoOptions = {}) {
144
142
  await route.continue(overrides);
145
143
  }
146
144
  catch (error) {
147
- log.debug('Error inside request interceptor', { error });
145
+ getLog().debug('Error inside request interceptor', { error });
148
146
  }
149
147
  return undefined;
150
148
  };
151
149
  await page.route('**/*', interceptRequestHandler);
152
150
  }
151
+ else if (!isEmpty(headers)) {
152
+ await page.setExtraHTTPHeaders(headers);
153
+ }
153
154
  return page.goto(url, gotoOptions);
154
155
  }
155
156
  /**
@@ -213,7 +214,7 @@ export async function blockRequests(page, options = {}) {
213
214
  await client.send('Network.setBlockedURLs', { urls: patternsToBlock });
214
215
  }
215
216
  catch {
216
- log.warning('blockRequests() helper is incompatible with non-Chromium browsers.');
217
+ getLog().warning('blockRequests() helper is incompatible with non-Chromium browsers.');
217
218
  }
218
219
  }
219
220
  /**
@@ -249,7 +250,7 @@ export function compileScript(scriptString, context = Object.create(null)) {
249
250
  func = vm.runInNewContext(funcString, context); // "Secure" the context by removing prototypes, unless custom context is provided.
250
251
  }
251
252
  catch (err) {
252
- log.exception(err, 'Cannot compile script!');
253
+ getLog().exception(err, 'Cannot compile script!');
253
254
  throw err;
254
255
  }
255
256
  if (typeof func !== 'function')
@@ -356,12 +357,12 @@ export async function saveSnapshot(page, options = {}) {
356
357
  saveScreenshot: ow.optional.boolean,
357
358
  saveHtml: ow.optional.boolean,
358
359
  keyValueStoreName: ow.optional.string,
359
- config: ow.optional.object,
360
+ configuration: ow.optional.object,
360
361
  }));
361
- const { key = 'SNAPSHOT', screenshotQuality = 50, saveScreenshot = true, saveHtml = true, keyValueStoreName, config, } = options;
362
+ const { key = 'SNAPSHOT', screenshotQuality = 50, saveScreenshot = true, saveHtml = true, keyValueStoreName, configuration, } = options;
362
363
  try {
363
- const store = await KeyValueStore.open(keyValueStoreName, {
364
- config: config ?? Configuration.getGlobalConfig(),
364
+ const store = await KeyValueStore.open(keyValueStoreName ? { name: keyValueStoreName } : null, {
365
+ configuration: configuration ?? Configuration.getGlobalConfiguration(),
365
366
  });
366
367
  if (saveScreenshot) {
367
368
  const screenshotName = `${key}.jpg`;
@@ -397,35 +398,70 @@ export async function saveSnapshot(page, options = {}) {
397
398
  */
398
399
  export async function parseWithCheerio(page, ignoreShadowRoots = false, ignoreIframes = false) {
399
400
  ow(page, ow.object.validate(validators.browserPage));
401
+ const html = ignoreShadowRoots
402
+ ? null
403
+ : (await page.evaluate(`(${expandShadowRoots.toString()})(document)`));
404
+ const pageContent = html || (await page.content());
405
+ const $ = cheerio.load(pageContent);
400
406
  if (page.frames().length > 1 && !ignoreIframes) {
401
407
  const frames = await page.$$('iframe');
402
- await Promise.all(frames.map(async (frame) => {
408
+ const cheerioIframes = $('iframe').toArray();
409
+ if (frames.length !== cheerioIframes.length) {
410
+ serviceLocator
411
+ .getLogger()
412
+ .warning(`parseWithCheerio: iframe count mismatch between live DOM (${frames.length}) and page snapshot (${cheerioIframes.length}). Some iframes may not be expanded.`);
413
+ }
414
+ await Promise.all(frames.map(async (frame, index) => {
403
415
  try {
404
416
  const iframe = await frame.contentFrame();
405
- if (iframe) {
406
- const contents = await iframe.content();
407
- await frame.evaluate((f, c) => {
408
- const replacementNode = document.createElement('div');
409
- replacementNode.innerHTML = c;
410
- replacementNode.className = 'crawlee-iframe-replacement';
411
- f.replaceWith(replacementNode);
412
- }, contents);
417
+ if (iframe && cheerioIframes[index]) {
418
+ const getIframeHTML = async () => {
419
+ try {
420
+ return iframe.locator('body').first().innerHTML();
421
+ }
422
+ catch {
423
+ return iframe.content();
424
+ }
425
+ };
426
+ const contents = await getIframeHTML();
427
+ $(cheerioIframes[index]).replaceWith(`<div class="crawlee-iframe-replacement">${contents}</div>`);
413
428
  }
414
429
  }
415
430
  catch (error) {
416
- log.warning(`Failed to extract iframe content: ${error}`);
431
+ getLog().warning(`Failed to extract iframe content: ${error}`);
417
432
  }
418
433
  }));
419
434
  }
420
- const html = ignoreShadowRoots
421
- ? null
422
- : (await page.evaluate(`(${expandShadowRoots.toString()})(document)`));
423
- const pageContent = html || (await page.content());
424
- return cheerio.load(pageContent);
435
+ return $;
436
+ }
437
+ let idcacPlaywright = null;
438
+ async function getIdcacPlaywright() {
439
+ if (idcacPlaywright)
440
+ return idcacPlaywright;
441
+ try {
442
+ idcacPlaywright = await import('idcac-playwright');
443
+ }
444
+ catch (error) {
445
+ getLog().warning(`Failed to import 'idcac-playwright'.
446
+
447
+ We recently made idcac-playwright an optional dependency due to licensing issues.
448
+ To use this feature, please install it manually by running
449
+
450
+ npm install idcac-playwright
451
+
452
+ Original error message follows:
453
+
454
+ ${error.message}
455
+ `);
456
+ }
457
+ return idcacPlaywright;
425
458
  }
426
459
  export async function closeCookieModals(page) {
427
460
  ow(page, ow.object.validate(validators.browserPage));
428
- await page.evaluate(getCookieClosingScript());
461
+ const idcac = await getIdcacPlaywright();
462
+ if (idcac?.getInjectableScript()) {
463
+ await page.evaluate(idcac.getInjectableScript());
464
+ }
429
465
  }
430
466
  /**
431
467
  * This helper tries to solve the Cloudflare challenge automatically by clicking on the checkbox.
@@ -434,30 +470,24 @@ export async function closeCookieModals(page) {
434
470
  * result in a SessionError which will be automatically retried, so only successful requests will get
435
471
  * into the `requestHandler`.
436
472
  *
473
+ * On a successfully solved challenge the page is reloaded and the new {@link Response} is returned, so
474
+ * it can be propagated back to the crawling context via a hook return value (see
475
+ * {@link handleCloudflareChallengeHook}).
476
+ *
437
477
  * Works best with camoufox.
438
478
  *
439
479
  * **Example usage**
440
480
  * ```ts
441
481
  * postNavigationHooks: [
442
- * async ({ handleCloudflareChallenge }) => {
443
- * await handleCloudflareChallenge();
444
- * },
482
+ * async (context) => ({ response: await context.handleCloudflareChallenge() }),
445
483
  * ],
446
484
  * ```
447
485
  *
448
486
  * @param page Playwright [`Page`](https://playwright.dev/docs/api/class-page) object
449
487
  * @param url current URL for request identification, only used for logging
450
- * @param [session] current session object
451
488
  * @param [options]
452
489
  */
453
- async function handleCloudflareChallenge(page, url, session, options = {}) {
454
- // eslint-disable-next-line dot-notation
455
- const blockedStatusCodes = session?.['sessionPool']['blockedStatusCodes'];
456
- // Cloudflare pages are 403, which are blocked by default
457
- if (blockedStatusCodes?.includes(403)) {
458
- const idx = blockedStatusCodes.indexOf(403);
459
- blockedStatusCodes.splice(idx, 1);
460
- }
490
+ async function handleCloudflareChallenge(page, url, options = {}) {
461
491
  options.isBlockedCallback ??= async () => {
462
492
  const isBlocked = await page.evaluate(() => {
463
493
  return document.querySelector('h1')?.textContent?.trim().includes('Sorry, you have been blocked');
@@ -466,7 +496,9 @@ async function handleCloudflareChallenge(page, url, session, options = {}) {
466
496
  };
467
497
  options.isChallengeCallback ??= async () => {
468
498
  return await page.evaluate(async () => {
469
- return !!document.querySelector('.footer > .footer-inner > .diagnostic-wrapper > .ray-id');
499
+ // Cloudflare nests the ray ID under varying wrapper elements, so we match by descendants
500
+ // instead of a direct-child chain (e.g. a `.footer-wrapper` was inserted in between).
501
+ return !!document.querySelector('.footer .footer-inner .diagnostic-wrapper .ray-id');
470
502
  });
471
503
  };
472
504
  const retryBlocked = async () => {
@@ -481,31 +513,41 @@ async function handleCloudflareChallenge(page, url, session, options = {}) {
481
513
  };
482
514
  if (!(await isChallenge())) {
483
515
  await retryBlocked();
484
- return;
516
+ return undefined;
485
517
  }
486
518
  const logLevel = options.verbose ? 'info' : 'debug';
487
- log[logLevel](`Detected Cloudflare challenge at ${url}, trying to solve it. This can take up to ${10 + (options.sleepSecs ?? 10)} seconds.`);
519
+ getLog()[logLevel](`Detected Cloudflare challenge at ${url}, trying to solve it. This can take up to ${10 + (options.sleepSecs ?? 10)} seconds.`);
488
520
  const bb = await page
489
521
  .evaluate(() => {
490
- const div = document.querySelector('.main-content div');
522
+ // Prefer the actual challenge widget (the box holding the Turnstile checkbox input);
523
+ // fall back to the first content div for older challenge layouts.
524
+ const div = document.querySelector('.main-content div:has(input[id^="cf-chl-widget-"])') ??
525
+ document.querySelector('.main-content div');
491
526
  return div?.getBoundingClientRect();
492
527
  })
493
528
  .catch(() => undefined);
494
529
  if (!bb) {
495
- return;
530
+ return undefined;
496
531
  }
497
532
  const randomOffset = (range) => {
498
533
  return Math.round(100 * range * Math.random()) / 100;
499
534
  };
500
- const x = bb.x + 30;
501
- const y = bb.y + 25;
535
+ let x = bb.x + 30;
536
+ let y = bb.y + 25;
502
537
  // try to click the checkbox every second
503
538
  for (let i = 0; i < 10; i++) {
504
- await sleep(1000);
539
+ await sleep((options.preChallengeSleepSecs ?? 1) * 1000);
505
540
  // break early if we are no longer on the CF challenge page
506
541
  if (!(await isChallenge())) {
507
542
  break;
508
543
  }
544
+ if (options.clickPositionCallback) {
545
+ const pos = await options.clickPositionCallback(page);
546
+ if (pos) {
547
+ x = pos.x;
548
+ y = pos.y;
549
+ }
550
+ }
509
551
  if (options.clickCallback) {
510
552
  await options.clickCallback(page, { x, y });
511
553
  continue;
@@ -513,7 +555,10 @@ async function handleCloudflareChallenge(page, url, session, options = {}) {
513
555
  // we can click on the text too, so X can be a bit larger
514
556
  const xRandomized = x + randomOffset(10);
515
557
  const yRandomized = y + randomOffset(10);
516
- log[logLevel](`Trying to click on the Cloudflare checkbox at ${url}`, { x: xRandomized, y: yRandomized });
558
+ getLog()[logLevel](`Trying to click on the Cloudflare checkbox at ${url}`, {
559
+ x: xRandomized,
560
+ y: yRandomized,
561
+ });
517
562
  await page.mouse.click(xRandomized, yRandomized);
518
563
  // sometimes the checkbox is lower (could be caused by a lag when rendering the logo)
519
564
  await page.mouse.click(xRandomized, yRandomized + 35);
@@ -523,40 +568,9 @@ async function handleCloudflareChallenge(page, url, session, options = {}) {
523
568
  throw new SessionError(`Blocked by Cloudflare when processing ${url}`);
524
569
  }
525
570
  await retryBlocked();
526
- }
527
- export function registerUtilsToContext(context, crawlerOptions) {
528
- context.injectFile = async (filePath, options) => injectFile(context.page, filePath, options);
529
- context.injectJQuery = async () => {
530
- if (context.request.state === RequestState.BEFORE_NAV) {
531
- log.warning('Using injectJQuery() in preNavigationHooks leads to unstable results. Use it in a postNavigationHook or a requestHandler instead.');
532
- await injectJQuery(context.page);
533
- return;
534
- }
535
- await injectJQuery(context.page, { surviveNavigations: false });
536
- };
537
- context.blockRequests = async (options) => blockRequests(context.page, options);
538
- context.waitForSelector = async (selector, timeoutMs = 5_000) => {
539
- const locator = context.page.locator(selector).first();
540
- await locator.waitFor({ timeout: timeoutMs, state: 'attached' });
541
- };
542
- context.parseWithCheerio = async (selector, timeoutMs = 5_000) => {
543
- if (selector) {
544
- await context.waitForSelector(selector, timeoutMs);
545
- }
546
- return parseWithCheerio(context.page, crawlerOptions.ignoreShadowRoots, crawlerOptions.ignoreIframes);
547
- };
548
- context.infiniteScroll = async (options) => infiniteScroll(context.page, options);
549
- context.saveSnapshot = async (options) => saveSnapshot(context.page, { ...options, config: context.crawler.config });
550
- context.enqueueLinksByClickingElements = async (options) => enqueueLinksByClickingElements({
551
- ...options,
552
- page: context.page,
553
- requestQueue: context.crawler.requestQueue,
554
- });
555
- context.compileScript = (scriptString, ctx) => compileScript(scriptString, ctx);
556
- context.closeCookieModals = async () => closeCookieModals(context.page);
557
- context.handleCloudflareChallenge = async (options) => {
558
- return handleCloudflareChallenge(context.page, context.request.url, context.session, options);
559
- };
571
+ // Reload to obtain a fresh Response without the challenge interstitial, which the caller can
572
+ // propagate back into the crawling context so downstream status-code checks see the new value.
573
+ return (await page.reload()) ?? undefined;
560
574
  }
561
575
  export { enqueueLinksByClickingElements };
562
576
  /** @internal */
@@ -574,4 +588,3 @@ export const playwrightUtils = {
574
588
  RenderingTypePredictor,
575
589
  handleCloudflareChallenge,
576
590
  };
577
- //# sourceMappingURL=playwright-utils.js.map
@@ -1,21 +1,38 @@
1
- import type { Request } from '@crawlee/core';
1
+ import type { RecoverableStatePersistenceOptions, Request } from '@crawlee/core';
2
2
  export type RenderingType = 'clientOnly' | 'static';
3
- type URLComponents = string[];
4
- type FeatureVector = [staticResultsSimilarity: number, clientOnlyResultsSimilarity: number];
5
3
  export interface RenderingTypePredictorOptions {
6
4
  /** A number between 0 and 1 that determines the desired ratio of rendering type detections */
7
5
  detectionRatio: number;
6
+ persistenceOptions?: Partial<RecoverableStatePersistenceOptions>;
7
+ }
8
+ /**
9
+ * Minimal contract that any object passed to {@link AdaptivePlaywrightCrawler} as its
10
+ * `renderingTypePredictor` option must satisfy.
11
+ *
12
+ * @experimental
13
+ */
14
+ export interface IRenderingTypePredictor {
15
+ /** Predict the rendering type for a request, and how likely the crawler should be to verify it. */
16
+ predict(request: Request): {
17
+ renderingType: RenderingType;
18
+ detectionProbabilityRecommendation: number;
19
+ };
20
+ /** Report a detected rendering type, so that future predictions can take it into account. */
21
+ storeResult(requests: Request | Request[], renderingType: RenderingType): void;
8
22
  }
9
23
  /**
10
24
  * Stores rendering type information for previously crawled URLs and predicts the rendering type for URLs that have yet to be crawled and recommends when rendering type detection should be performed.
11
25
  *
12
26
  * @experimental
13
27
  */
14
- export declare class RenderingTypePredictor {
15
- private renderingTypeDetectionResults;
28
+ export declare class RenderingTypePredictor implements IRenderingTypePredictor {
16
29
  private detectionRatio;
17
- private logreg;
18
- constructor({ detectionRatio }: RenderingTypePredictorOptions);
30
+ private state;
31
+ constructor({ detectionRatio, persistenceOptions }: RenderingTypePredictorOptions);
32
+ /**
33
+ * Initialize the predictor by restoring persisted state.
34
+ */
35
+ initialize(): Promise<void>;
19
36
  /**
20
37
  * Predict the rendering type for a given URL and request label.
21
38
  */
@@ -26,10 +43,8 @@ export declare class RenderingTypePredictor {
26
43
  /**
27
44
  * Store the rendering type for a given URL and request label. This updates the underlying prediction model, which may be costly.
28
45
  */
29
- storeResult({ url, loadedUrl, label }: Request, renderingType: RenderingType): void;
46
+ storeResult(requests: Request | Request[], renderingType: RenderingType): void;
30
47
  private resultCount;
31
- protected calculateFeatureVector(url: URLComponents, label: string | undefined): FeatureVector;
32
- protected retrain(): void;
48
+ private calculateFeatureVector;
49
+ private retrain;
33
50
  }
34
- export {};
35
- //# sourceMappingURL=rendering-type-prediction.d.ts.map