@clerk/testing
Version:
Utilities to help you create E2E test suites for apps using Clerk
1 lines • 8.56 kB
Source Map (JSON)
{"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 // Diagnostics are best-effort: never let them prevent fulfilling the response.\n let diagnostics = '';\n try {\n const headers = response.headers();\n diagnostics = `\\n cf-ray: ${headers['cf-ray'] ?? 'n/a'}, retry-after: ${headers['retry-after'] ?? 'n/a'}, content-type: ${headers['content-type'] ?? 'n/a'}`;\n diagnostics += `\\n body: ${(await response.text()).slice(0, 500)}`;\n } catch {\n // ignore\n }\n console.warn(\n `[Clerk Testing] FAPI request failed with status ${status} after ${MAX_ROUTE_RETRIES + 1} attempts: ${route.request().url()}${diagnostics}`,\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()} (${String(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":"gDAWA,IAAMA,EAAgB,IAAI,QAEpBC,EAAyB,IAAI,IAAI,CAAC,IAAK,IAAK,IAAK,GAAG,CAAC,EACrDC,EAAoB,EACpBC,EAAgB,IAChBC,EAAgB,IAqBTC,EAAyB,MAAO,CAAE,QAAAC,EAAS,QAAAC,EAAS,KAAAC,CAAK,IAAoC,CACxG,IAAMC,EAAiBH,GAAWE,GAAM,QAAQ,EAEhD,GAAI,CAACC,EACH,MAAM,IAAI,MAAM,gEAAgE,EAGlF,GAAIT,EAAc,IAAIS,CAAc,EAAG,CACjC,QAAQ,IAAI,qBACd,QAAQ,IAAI,6FAA6F,EAE3G,MACF,CAEA,IAAMC,EAAUH,GAAS,gBAAkB,QAAQ,IAAI,WACvD,GAAI,CAACG,EACH,MAAM,IAAI,MAAMC,CAA8B,EAGhD,IAAMC,EAAiBF,EAAQ,QAAQ,sBAAuB,MAAM,EAC9DG,EAAS,IAAI,OAAO,YAAYD,CAAc,kBAAkB,EAEtEZ,EAAc,IAAIS,CAAc,EAChC,GAAI,CACF,MAAMA,EAAe,MAAMI,EAAQ,MAAMC,GAAS,CAChD,IAAMC,EAAc,IAAI,IAAID,EAAM,QAAQ,EAAE,IAAI,CAAC,EAC3CE,EAAe,QAAQ,IAAI,oBAE7BA,GACFD,EAAY,aAAa,IAAIE,EAAqBD,CAAY,EAGhE,IAAME,EAAYH,EAAY,SAAS,EAEvC,QAASI,EAAU,EAAGA,GAAWjB,EAAmBiB,IAClD,GAAI,CACF,IAAMC,EAAW,MAAMN,EAAM,MAAM,CAAE,IAAKI,CAAU,CAAC,EAC/CG,EAASD,EAAS,OAAO,EAE/B,GAAInB,EAAuB,IAAIoB,CAAM,EAAG,CACtC,GAAIF,EAAUjB,EAAmB,CAC/B,IAAMoB,EAAQnB,EAAgB,KAAK,IAAI,EAAGgB,CAAO,EAAI,KAAK,OAAO,EAAIf,EACjE,QAAQ,IAAI,qBACd,QAAQ,IACN,iCAAiCiB,CAAM,uBAAuBF,EAAU,CAAC,IAAIjB,CAAiB,WAAW,KAAK,MAAMoB,CAAK,CAAC,QAAQR,EAAM,QAAQ,EAAE,IAAI,CAAC,EACzJ,EAEF,MAAM,IAAI,QAAQS,GAAW,WAAWA,EAASD,CAAK,CAAC,EACvD,QACF,CAGA,IAAIE,EAAc,GAClB,GAAI,CACF,IAAMC,EAAUL,EAAS,QAAQ,EACjCI,EAAc;AAAA,YAAeC,EAAQ,QAAQ,GAAK,KAAK,kBAAkBA,EAAQ,aAAa,GAAK,KAAK,mBAAmBA,EAAQ,cAAc,GAAK,KAAK,GAC3JD,GAAe;AAAA,WAAc,MAAMJ,EAAS,KAAK,GAAG,MAAM,EAAG,GAAG,CAAC,EACnE,MAAQ,CAER,CACA,QAAQ,KACN,mDAAmDC,CAAM,UAAUnB,EAAoB,CAAC,cAAcY,EAAM,QAAQ,EAAE,IAAI,CAAC,GAAGU,CAAW,EAC3I,EACA,MAAMV,EAAM,QAAQ,CAAE,SAAAM,CAAS,CAAC,EAChC,MACF,CAEA,IAAMM,EAAO,MAAMN,EAAS,KAAK,EAG7BM,GAAM,UAAU,iBAAmB,KACrCA,EAAK,SAAS,eAAiB,IAI7BA,GAAM,QAAQ,iBAAmB,KACnCA,EAAK,OAAO,eAAiB,IAG/B,MAAMZ,EAAM,QAAQ,CAAE,SAAAM,EAAU,KAAAM,CAAK,CAAC,EACtC,MACF,OAASC,EAAO,CACd,GAAIR,EAAUjB,EAAmB,CAC/B,IAAMoB,EAAQnB,EAAgB,KAAK,IAAI,EAAGgB,CAAO,EAAI,KAAK,OAAO,EAAIf,EACjE,QAAQ,IAAI,qBACd,QAAQ,IACN,yDAAyDe,EAAU,CAAC,IAAIjB,CAAiB,WAAW,KAAK,MAAMoB,CAAK,CAAC,QAAQR,EAAM,QAAQ,EAAE,IAAI,CAAC,GAClJa,CACF,EAEF,MAAM,IAAI,QAAQJ,GAAW,WAAWA,EAASD,CAAK,CAAC,EACvD,QACF,CAEA,QAAQ,KACN,6CAA6CpB,EAAoB,CAAC,cAAcY,EAAM,QAAQ,EAAE,IAAI,CAAC,KAAK,OAAOa,CAAK,CAAC,GACzH,EACA,MAAMb,EAAM,SAAS,CAAE,IAAKI,CAAU,CAAC,EAAE,MAAM,QAAQ,KAAK,EAC5D,MACF,CAEJ,CAAC,CACH,OAAS,EAAG,CACV,MAAAlB,EAAc,OAAOS,CAAc,EAC7B,CACR,CACF","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","diagnostics","headers","json","error"]}