UNPKG

@clerk/testing

Version:

Utilities to help you create E2E test suites for apps using Clerk

1 lines 7.75 kB
{"version":3,"sources":["../src/playwright/setupClerkTestingToken.ts"],"sourcesContent":["import type { BrowserContext, Page } from '@playwright/test';\n\nimport type { SetupClerkTestingTokenOptions } from '../common';\nimport { ERROR_MISSING_FRONTEND_API_URL, TESTING_TOKEN_PARAM } from '../common';\n\ntype SetupClerkTestingTokenParams = {\n context?: BrowserContext;\n page?: Page;\n options?: SetupClerkTestingTokenOptions;\n};\n\nconst setupContexts = new WeakSet<BrowserContext>();\n\nconst RETRYABLE_STATUS_CODES = new Set([429, 502, 503, 504]);\nconst MAX_ROUTE_RETRIES = 3;\nconst BASE_DELAY_MS = 500;\nconst JITTER_MAX_MS = 250;\n\n/**\n * Bypasses bot protection by appending the testing token in the Frontend API requests.\n *\n * @param params.context - The Playwright browser context object.\n * @param params.page - The Playwright page object.\n * @param params.options.frontendApiUrl - The frontend API URL for your Clerk dev instance, without the protocol.\n * @returns A promise that resolves when the bot protection bypass is set up.\n * @throws An error if the Frontend API URL is not provided.\n * @remarks Set the `CLERK_TESTING_DEBUG` environment variable to enable verbose logging of retry attempts and route handler registration.\n * @example\n * import { setupClerkTestingToken } from '@clerk/testing/playwright';\n *\n * test('should bypass bot protection', async ({ context }) => {\n * await setupClerkTestingToken({ context });\n * const page = await context.newPage();\n * await page.goto('https://your-app.com');\n * // Continue with your test...\n * });\n */\nexport const setupClerkTestingToken = async ({ context, options, page }: SetupClerkTestingTokenParams) => {\n const browserContext = context ?? page?.context();\n\n if (!browserContext) {\n throw new Error('Either context or page must be provided to setup testing token');\n }\n\n if (setupContexts.has(browserContext)) {\n if (process.env.CLERK_TESTING_DEBUG) {\n console.log('[Clerk Testing] Route handler already registered for this context, skipping duplicate setup');\n }\n return;\n }\n\n const fapiUrl = options?.frontendApiUrl || process.env.CLERK_FAPI;\n if (!fapiUrl) {\n throw new Error(ERROR_MISSING_FRONTEND_API_URL);\n }\n\n const escapedFapiUrl = fapiUrl.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n const apiUrl = new RegExp(`^https://${escapedFapiUrl}/v1/.*?(\\\\?.*)?$`);\n\n setupContexts.add(browserContext);\n try {\n await browserContext.route(apiUrl, async route => {\n const originalUrl = new URL(route.request().url());\n const testingToken = process.env.CLERK_TESTING_TOKEN;\n\n if (testingToken) {\n originalUrl.searchParams.set(TESTING_TOKEN_PARAM, testingToken);\n }\n\n const urlString = originalUrl.toString();\n\n for (let attempt = 0; attempt <= MAX_ROUTE_RETRIES; attempt++) {\n try {\n const response = await route.fetch({ url: urlString });\n const status = response.status();\n\n if (RETRYABLE_STATUS_CODES.has(status)) {\n if (attempt < MAX_ROUTE_RETRIES) {\n const delay = BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * JITTER_MAX_MS;\n if (process.env.CLERK_TESTING_DEBUG) {\n console.log(\n `[Clerk Testing] FAPI returned ${status}, retrying (attempt ${attempt + 1}/${MAX_ROUTE_RETRIES}, delay ${Math.round(delay)}ms): ${route.request().url()}`,\n );\n }\n await new Promise(resolve => setTimeout(resolve, delay));\n continue;\n }\n\n console.warn(\n `[Clerk Testing] FAPI request failed with status ${status} after ${MAX_ROUTE_RETRIES + 1} attempts: ${route.request().url()}`,\n );\n await route.fulfill({ response });\n return;\n }\n\n const json = await response.json();\n\n // Override captcha_bypass in /v1/client\n if (json?.response?.captcha_bypass === false) {\n json.response.captcha_bypass = true;\n }\n\n // Override captcha_bypass in piggybacking\n if (json?.client?.captcha_bypass === false) {\n json.client.captcha_bypass = true;\n }\n\n await route.fulfill({ response, json });\n return;\n } catch (error) {\n if (attempt < MAX_ROUTE_RETRIES) {\n const delay = BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * JITTER_MAX_MS;\n if (process.env.CLERK_TESTING_DEBUG) {\n console.log(\n `[Clerk Testing] FAPI request error, retrying (attempt ${attempt + 1}/${MAX_ROUTE_RETRIES}, delay ${Math.round(delay)}ms): ${route.request().url()}`,\n error,\n );\n }\n await new Promise(resolve => setTimeout(resolve, delay));\n continue;\n }\n\n console.warn(\n `[Clerk Testing] FAPI request failed after ${MAX_ROUTE_RETRIES + 1} attempts: ${route.request().url()}`,\n error,\n );\n await route.continue({ url: urlString }).catch(console.error);\n return;\n }\n }\n });\n } catch (e) {\n setupContexts.delete(browserContext);\n throw e;\n }\n};\n"],"mappings":"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","names":["setupContexts","RETRYABLE_STATUS_CODES","MAX_ROUTE_RETRIES","BASE_DELAY_MS","JITTER_MAX_MS","setupClerkTestingToken","context","options","page","browserContext","fapiUrl","ERROR_MISSING_FRONTEND_API_URL","escapedFapiUrl","apiUrl","route","originalUrl","testingToken","TESTING_TOKEN_PARAM","urlString","attempt","response","status","delay","resolve","json","error"]}