From a3499a0a060acc0421b175da1ff9e391f85a7ef5 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 19 Mar 2026 09:38:30 -0500 Subject: [PATCH 1/5] test(e2e): pin Next.js to 16.2.0 to confirm cache-components breakage --- integration/templates/next-cache-components/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/templates/next-cache-components/package.json b/integration/templates/next-cache-components/package.json index 9a60805159f..3490305cf8b 100644 --- a/integration/templates/next-cache-components/package.json +++ b/integration/templates/next-cache-components/package.json @@ -13,7 +13,7 @@ "@types/node": "^18.19.33", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", - "next": "^16.0.0-canary.0", + "next": "16.2.0", "react": "^19.0.0", "react-dom": "^19.0.0", "typescript": "^5.7.3" From 6b7cf6a8657aea7c50c572d61baf7d79de2e0354 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 19 Mar 2026 09:53:13 -0500 Subject: [PATCH 2/5] test(e2e): add diagnostics to understand 16.2.0 failure mode --- integration/tests/cache-components.test.ts | 51 ++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/integration/tests/cache-components.test.ts b/integration/tests/cache-components.test.ts index 4c57fd778ae..be80a1a6d04 100644 --- a/integration/tests/cache-components.test.ts +++ b/integration/tests/cache-components.test.ts @@ -44,10 +44,61 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes], withPattern: // Sign in first await u.po.signIn.goTo(); + + // Diagnostic: check for duplicate DOM elements (Activity component issue) + const identifierInputs = await page.locator('input[name=identifier]').count(); + console.log(`[DIAG] identifier inputs in DOM: ${identifierInputs}`); + console.log(`[DIAG] URL after goTo sign-in: ${page.url()}`); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password, + waitForSession: false, }); + + // Diagnostic: capture state after clicking Continue but before waiting for session + const diagAfterSubmit = await page.evaluate(() => { + return { + url: window.location.href, + clerkDefined: typeof window.Clerk !== 'undefined', + clerkLoaded: !!(window as any).Clerk?.loaded, + clerkVersion: (window as any).Clerk?.version, + hasSession: !!(window as any).Clerk?.session, + hasUser: !!(window as any).Clerk?.user, + sessionId: (window as any).Clerk?.session?.id ?? null, + cookies: document.cookie, + identifierInputCount: document.querySelectorAll('input[name=identifier]').length, + signInRootCount: document.querySelectorAll('.cl-signIn-root').length, + activityElements: document.querySelectorAll('[data-activity]').length, + hiddenElements: document.querySelectorAll('[hidden]').length, + }; + }); + console.log('[DIAG] State after submit:', JSON.stringify(diagAfterSubmit, null, 2)); + + // Now wait for session with extended timeout and more diagnostics + try { + await page.waitForFunction( + () => !!window.Clerk?.session, + { timeout: 15_000 }, + ); + } catch { + // Capture state at timeout for debugging + const diagAtTimeout = await page.evaluate(() => { + return { + url: window.location.href, + clerkDefined: typeof window.Clerk !== 'undefined', + clerkLoaded: !!(window as any).Clerk?.loaded, + hasSession: !!(window as any).Clerk?.session, + hasUser: !!(window as any).Clerk?.user, + clerkStatus: (window as any).Clerk?.status, + cookies: document.cookie, + bodyHTML: document.body.innerHTML.substring(0, 2000), + }; + }); + console.log('[DIAG] State at TIMEOUT:', JSON.stringify(diagAtTimeout, null, 2)); + throw new Error(`waitForSession timed out. Diagnostics logged above.`); + } + await u.po.expect.toBeSignedIn(); // Navigate to server component page From c2f254290bf8d3f90a3345eb956dc33c43789466 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 19 Mar 2026 10:05:34 -0500 Subject: [PATCH 3/5] test(e2e): add detailed form interaction diagnostics for 16.2.0 --- integration/tests/cache-components.test.ts | 106 +++++++++++++-------- 1 file changed, 68 insertions(+), 38 deletions(-) diff --git a/integration/tests/cache-components.test.ts b/integration/tests/cache-components.test.ts index be80a1a6d04..7177144fb48 100644 --- a/integration/tests/cache-components.test.ts +++ b/integration/tests/cache-components.test.ts @@ -42,61 +42,91 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes], withPattern: test('auth() in server component works when signed in', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); + // Collect console errors and network failures + const consoleErrors: string[] = []; + const networkErrors: string[] = []; + page.on('console', msg => { + if (msg.type() === 'error') consoleErrors.push(msg.text()); + }); + page.on('requestfailed', req => { + networkErrors.push(`${req.method()} ${req.url()} - ${req.failure()?.errorText}`); + }); + // Sign in first await u.po.signIn.goTo(); - - // Diagnostic: check for duplicate DOM elements (Activity component issue) - const identifierInputs = await page.locator('input[name=identifier]').count(); - console.log(`[DIAG] identifier inputs in DOM: ${identifierInputs}`); console.log(`[DIAG] URL after goTo sign-in: ${page.url()}`); - await u.po.signIn.signInWithEmailAndInstantPassword({ - email: fakeUser.email, - password: fakeUser.password, - waitForSession: false, + // Check form state before interaction + const identifierInput = page.locator('input[name=identifier]'); + const isIdentifierVisible = await identifierInput.isVisible(); + const isIdentifierEnabled = await identifierInput.isEnabled(); + console.log(`[DIAG] identifier visible: ${isIdentifierVisible}, enabled: ${isIdentifierEnabled}`); + + // Fill identifier and check if password field appears + await identifierInput.fill(fakeUser.email); + const passwordInput = page.locator('input[name=password]'); + try { + await passwordInput.waitFor({ state: 'visible', timeout: 5000 }); + console.log('[DIAG] password field appeared after filling identifier'); + } catch { + console.log('[DIAG] password field did NOT appear after 5s'); + // Capture what the form looks like + const formHTML = await page.locator('.cl-signIn-root').innerHTML(); + console.log('[DIAG] sign-in form HTML:', formHTML.substring(0, 3000)); + } + + // Fill password and check Continue button + const isPasswordVisible = await passwordInput.isVisible(); + console.log(`[DIAG] password visible: ${isPasswordVisible}`); + if (isPasswordVisible) { + await passwordInput.fill(fakeUser.password, { force: true }); + } + + const continueBtn = page.getByRole('button', { name: 'Continue', exact: true }); + const isContinueVisible = await continueBtn.isVisible(); + const isContinueEnabled = await continueBtn.isEnabled(); + console.log(`[DIAG] continue button visible: ${isContinueVisible}, enabled: ${isContinueEnabled}`); + + // Track API calls during sign-in + const apiCalls: string[] = []; + page.on('response', res => { + if (res.url().includes('clerk') || res.url().includes('sign_in')) { + apiCalls.push(`${res.status()} ${res.url().split('?')[0]}`); + } }); - // Diagnostic: capture state after clicking Continue but before waiting for session - const diagAfterSubmit = await page.evaluate(() => { + // Click continue + await continueBtn.click(); + + // Wait a few seconds for the sign-in to process + await page.waitForTimeout(5000); + + const diagAfterWait = await page.evaluate(() => { return { url: window.location.href, - clerkDefined: typeof window.Clerk !== 'undefined', clerkLoaded: !!(window as any).Clerk?.loaded, - clerkVersion: (window as any).Clerk?.version, hasSession: !!(window as any).Clerk?.session, hasUser: !!(window as any).Clerk?.user, - sessionId: (window as any).Clerk?.session?.id ?? null, cookies: document.cookie, - identifierInputCount: document.querySelectorAll('input[name=identifier]').length, - signInRootCount: document.querySelectorAll('.cl-signIn-root').length, - activityElements: document.querySelectorAll('[data-activity]').length, - hiddenElements: document.querySelectorAll('[hidden]').length, + signInCardClass: document.querySelector('.cl-cardBox')?.className ?? 'NOT_FOUND', }; }); - console.log('[DIAG] State after submit:', JSON.stringify(diagAfterSubmit, null, 2)); + console.log('[DIAG] State 5s after click:', JSON.stringify(diagAfterWait, null, 2)); + console.log('[DIAG] API calls:', JSON.stringify(apiCalls)); + console.log('[DIAG] Console errors:', JSON.stringify(consoleErrors)); + console.log('[DIAG] Network errors:', JSON.stringify(networkErrors)); - // Now wait for session with extended timeout and more diagnostics + // Now wait for session try { - await page.waitForFunction( - () => !!window.Clerk?.session, - { timeout: 15_000 }, - ); + await page.waitForFunction(() => !!window.Clerk?.session, { timeout: 10_000 }); } catch { - // Capture state at timeout for debugging - const diagAtTimeout = await page.evaluate(() => { - return { - url: window.location.href, - clerkDefined: typeof window.Clerk !== 'undefined', - clerkLoaded: !!(window as any).Clerk?.loaded, - hasSession: !!(window as any).Clerk?.session, - hasUser: !!(window as any).Clerk?.user, - clerkStatus: (window as any).Clerk?.status, - cookies: document.cookie, - bodyHTML: document.body.innerHTML.substring(0, 2000), - }; - }); - console.log('[DIAG] State at TIMEOUT:', JSON.stringify(diagAtTimeout, null, 2)); - throw new Error(`waitForSession timed out. Diagnostics logged above.`); + const finalState = await page.evaluate(() => ({ + url: window.location.href, + hasSession: !!(window as any).Clerk?.session, + cookies: document.cookie, + })); + console.log('[DIAG] FINAL state at timeout:', JSON.stringify(finalState)); + throw new Error('waitForSession timed out'); } await u.po.expect.toBeSignedIn(); From e9dfc018d709fd9bf1ba747ff819c87f3d46b9cf Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 19 Mar 2026 10:33:41 -0500 Subject: [PATCH 4/5] test(e2e): trace password field events and React state under Activity --- integration/tests/cache-components.test.ts | 77 +++++++++++++++++++--- 1 file changed, 67 insertions(+), 10 deletions(-) diff --git a/integration/tests/cache-components.test.ts b/integration/tests/cache-components.test.ts index 7177144fb48..477c8fe213b 100644 --- a/integration/tests/cache-components.test.ts +++ b/integration/tests/cache-components.test.ts @@ -70,45 +70,102 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes], withPattern: console.log('[DIAG] password field appeared after filling identifier'); } catch { console.log('[DIAG] password field did NOT appear after 5s'); - // Capture what the form looks like const formHTML = await page.locator('.cl-signIn-root').innerHTML(); console.log('[DIAG] sign-in form HTML:', formHTML.substring(0, 3000)); } - // Fill password and check Continue button + // Install event listeners on password input BEFORE filling + await page.evaluate(() => { + const pwInput = document.querySelector('input[name=password]') as HTMLInputElement; + if (pwInput) { + (window as any).__pwEvents = []; + ['input', 'change', 'focus', 'blur', 'keydown', 'keyup'].forEach(evt => { + pwInput.addEventListener(evt, (e: Event) => { + (window as any).__pwEvents.push({ + type: e.type, + value: (e.target as HTMLInputElement).value.length, + isTrusted: e.isTrusted, + }); + }); + }); + // Also track React's synthetic event by monkey-patching the value setter + const origDescriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); + (window as any).__valueSetCount = 0; + if (origDescriptor?.set) { + Object.defineProperty(pwInput, 'value', { + set(val: string) { + (window as any).__valueSetCount++; + origDescriptor.set!.call(this, val); + }, + get() { + return origDescriptor.get!.call(this); + }, + }); + } + } + }); + + // Fill password const isPasswordVisible = await passwordInput.isVisible(); console.log(`[DIAG] password visible: ${isPasswordVisible}`); if (isPasswordVisible) { await passwordInput.fill(fakeUser.password, { force: true }); } + // Check what events fired and the password field state + const pwDiag = await page.evaluate(() => { + const pwInput = document.querySelector('input[name=password]') as HTMLInputElement; + return { + domValue: pwInput?.value ?? 'NOT_FOUND', + domValueLength: pwInput?.value?.length ?? 0, + events: (window as any).__pwEvents ?? [], + valueSetCount: (window as any).__valueSetCount ?? 0, + // Check password field's computed styles (Activity hiding?) + computedDisplay: pwInput ? getComputedStyle(pwInput).display : 'N/A', + computedOpacity: pwInput ? getComputedStyle(pwInput).opacity : 'N/A', + computedPointerEvents: pwInput ? getComputedStyle(pwInput).pointerEvents : 'N/A', + // Check parent container styles + parentOpacity: pwInput?.closest('[class*="instant"]') + ? getComputedStyle(pwInput.closest('[class*="instant"]')!).opacity + : pwInput?.parentElement + ? getComputedStyle(pwInput.parentElement).opacity + : 'N/A', + }; + }); + console.log('[DIAG] Password field after fill:', JSON.stringify(pwDiag, null, 2)); + const continueBtn = page.getByRole('button', { name: 'Continue', exact: true }); const isContinueVisible = await continueBtn.isVisible(); const isContinueEnabled = await continueBtn.isEnabled(); console.log(`[DIAG] continue button visible: ${isContinueVisible}, enabled: ${isContinueEnabled}`); - // Track API calls during sign-in + // Track API calls with response bodies for sign-in calls const apiCalls: string[] = []; - page.on('response', res => { - if (res.url().includes('clerk') || res.url().includes('sign_in')) { - apiCalls.push(`${res.status()} ${res.url().split('?')[0]}`); + page.on('response', async res => { + const url = res.url(); + if (url.includes('sign_in')) { + try { + const body = await res.json(); + const status = body?.response?.status || body?.status || 'unknown'; + apiCalls.push(`${res.status()} ${url.split('?')[0].split('/').slice(-2).join('/')} signInStatus=${status}`); + } catch { + apiCalls.push(`${res.status()} ${url.split('?')[0].split('/').slice(-2).join('/')}`); + } } }); // Click continue await continueBtn.click(); - // Wait a few seconds for the sign-in to process + // Wait for the sign-in to process await page.waitForTimeout(5000); const diagAfterWait = await page.evaluate(() => { return { url: window.location.href, - clerkLoaded: !!(window as any).Clerk?.loaded, hasSession: !!(window as any).Clerk?.session, - hasUser: !!(window as any).Clerk?.user, - cookies: document.cookie, signInCardClass: document.querySelector('.cl-cardBox')?.className ?? 'NOT_FOUND', + signInStatus: (window as any).Clerk?.client?.signIn?.status ?? 'N/A', }; }); console.log('[DIAG] State 5s after click:', JSON.stringify(diagAfterWait, null, 2)); From 879aaad26139eec5ef5594e1c713da805318f8b0 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 19 Mar 2026 11:17:58 -0500 Subject: [PATCH 5/5] fix(nextjs): skip invalidateCacheAction on Next.js 16 to unblock setActive On Next.js 16.2.0 with cacheComponents, the invalidateCacheAction server action (which calls cookies() from next/headers) hangs indefinitely, blocking setActive() from completing after sign-in. The sign-in API returns complete but the session is never set because setActive is stuck awaiting the server action. Skip the server action on Next.js 16 and rely on router.refresh() in onAfterSetActive for cache invalidation instead. --- packages/nextjs/src/app-router/client/ClerkProvider.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/nextjs/src/app-router/client/ClerkProvider.tsx b/packages/nextjs/src/app-router/client/ClerkProvider.tsx index 109e1a38c87..e440576c4c6 100644 --- a/packages/nextjs/src/app-router/client/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/client/ClerkProvider.tsx @@ -63,6 +63,11 @@ const NextClientClerkProvider = (props: NextClerkProviderPr // Once `setActive` performs the navigation, `__internal_onAfterSetActive` will kick in and perform a router.refresh ensuring shared layouts will also update with the correct authentication context. if ((nextVersion.startsWith('15') || nextVersion.startsWith('16')) && intent === 'sign-out') { resolve(); // noop + } else if (nextVersion.startsWith('16')) { + // On Next.js 16 with cacheComponents, calling invalidateCacheAction (a server action that + // calls cookies()) hangs indefinitely, blocking setActive from completing. The router.refresh() + // in onAfterSetActive is sufficient to invalidate the cache on Next.js 16+. + resolve(); } else { void invalidateCacheAction().then(() => resolve()); }