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" diff --git a/integration/tests/cache-components.test.ts b/integration/tests/cache-components.test.ts index 4c57fd778ae..477c8fe213b 100644 --- a/integration/tests/cache-components.test.ts +++ b/integration/tests/cache-components.test.ts @@ -42,12 +42,150 @@ 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(); - await u.po.signIn.signInWithEmailAndInstantPassword({ - email: fakeUser.email, - password: fakeUser.password, + console.log(`[DIAG] URL after goTo sign-in: ${page.url()}`); + + // 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'); + const formHTML = await page.locator('.cl-signIn-root').innerHTML(); + console.log('[DIAG] sign-in form HTML:', formHTML.substring(0, 3000)); + } + + // 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 with response bodies for sign-in calls + const apiCalls: string[] = []; + 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 for the sign-in to process + await page.waitForTimeout(5000); + + const diagAfterWait = await page.evaluate(() => { + return { + url: window.location.href, + hasSession: !!(window as any).Clerk?.session, + 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)); + 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 + try { + await page.waitForFunction(() => !!window.Clerk?.session, { timeout: 10_000 }); + } catch { + 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(); // Navigate to server component page 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()); }