diff --git a/packages/playwright-crawler/src/internals/utils/playwright-utils.ts b/packages/playwright-crawler/src/internals/utils/playwright-utils.ts index c7295524b6b2..5fdb3bb89c81 100644 --- a/packages/playwright-crawler/src/internals/utils/playwright-utils.ts +++ b/packages/playwright-crawler/src/internals/utils/playwright-utils.ts @@ -755,9 +755,9 @@ async function handleCloudflareChallenge( options.isChallengeCallback ??= async () => { return await page.evaluate(async () => { - // Cloudflare nests the ray ID under varying wrapper elements, so we match by descendants - // instead of a direct-child chain (e.g. a `.footer-wrapper` was inserted in between). - return !!document.querySelector('.footer .footer-inner .diagnostic-wrapper .ray-id'); + // Cloudflare keeps reshuffling the wrapper elements between `.footer-inner` and `.ray-id`, + // so only the stable outer classes are matched. + return !!document.querySelector('.footer .footer-inner .ray-id'); }); }; diff --git a/test/core/playwright_utils.test.ts b/test/core/playwright_utils.test.ts index b765e5462739..83ab2a8a5e55 100644 --- a/test/core/playwright_utils.test.ts +++ b/test/core/playwright_utils.test.ts @@ -433,4 +433,70 @@ describe('playwrightUtils', () => { await browser.close(); } }); + + describe('handleCloudflareChallenge() challenge detection', () => { + // not a named export, only reachable via the internal `playwrightUtils` object + const { handleCloudflareChallenge } = playwrightUtils.playwrightUtils; + let browser: Browser; + let page: Page; + + // no clicking or waiting, so an unsolved challenge fails fast instead of spending ~20s in the click loop + const fastOptions = () => ({ + sleepSecs: 0, + preChallengeSleepSecs: 0, + clickCallback: async () => {}, + }); + + // markup as rendered by the current (2026) Cloudflare challenge template + const currentChallengeBody = ` +
+

example.com

+

Performing security verification

+
+
+ `; + + // the older template, with the `.diagnostic-wrapper` element + const oldChallengeBody = ` +
+

example.com

+
+
+ `; + + beforeAll(async () => { + browser = await chromium.launch({ headless: true }); + page = await browser.newPage(); + }); + + afterAll(async () => { + await browser.close(); + }); + + test('detects the current challenge markup', async () => { + await page.setContent(`${currentChallengeBody}`); + await expect( + handleCloudflareChallenge(page, 'https://example.com', undefined, fastOptions()), + ).rejects.toThrow(/Blocked by Cloudflare/); + }); + + test('detects the old challenge markup', async () => { + await page.setContent(`${oldChallengeBody}`); + await expect( + handleCloudflareChallenge(page, 'https://example.com', undefined, fastOptions()), + ).rejects.toThrow(/Blocked by Cloudflare/); + }); + + test('resolves without detection on a regular page', async () => { + await page.setContent('

Welcome

'); + await expect( + handleCloudflareChallenge(page, 'https://example.com', undefined, fastOptions()), + ).resolves.toBeUndefined(); + }); + }); }); diff --git a/test/e2e/camoufox-cloudflare/actor/main.js b/test/e2e/camoufox-cloudflare/actor/main.js index 4a55d2e8cbb8..3667ebd6ae82 100644 --- a/test/e2e/camoufox-cloudflare/actor/main.js +++ b/test/e2e/camoufox-cloudflare/actor/main.js @@ -52,7 +52,7 @@ await Actor.main(async () => { async requestHandler({ page, parseWithCheerio }) { const isBlocked = await page .evaluate(async () => { - return !!document.querySelector('.footer .footer-inner .diagnostic-wrapper .ray-id'); + return !!document.querySelector('.footer .footer-inner .ray-id'); }) .catch(() => false); const $ = await parseWithCheerio();