Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion integration/templates/next-cache-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
144 changes: 141 additions & 3 deletions integration/tests/cache-components.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Comment on lines +68 to +72
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don’t let a pre-sign-in failure masquerade as the target regression.

If the password step never renders, this test still clicks Continue and can later throw waitForSession timed out. That makes an upstream form/rendering failure indistinguishable from the specific post-sign-in window.Clerk?.session regression this PR is supposed to isolate.

Suggested fix
       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));
+        throw new Error('Password step never rendered; aborting before session diagnostics');
       }

-      const isPasswordVisible = await passwordInput.isVisible();
-      console.log(`[DIAG] password visible: ${isPasswordVisible}`);
-      if (isPasswordVisible) {
-        await passwordInput.fill(fakeUser.password, { force: true });
-      }
+      await passwordInput.fill(fakeUser.password, { force: true });

Also applies to: 79-99, 119-129

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@integration/tests/cache-components.test.ts` around lines 68 - 72, The
diagnostic try/catch around passwordInput.waitFor(...) is allowing the test to
continue when the password step never appears, masking upstream render failures
as the regression under test; modify the blocks that call passwordInput.waitFor
(the ones around the "Continue" click and the later similar blocks at lines
referenced) to throw or explicitly fail the test when the wait times out instead
of only logging—i.e., replace the silent catch with a test.fail/assertion (or
rethrow) so the test stops immediately if passwordInput.waitFor(...) does not
resolve, preventing later waitForSession timeouts from being misattributed to
the targeted window.Clerk?.session regression.

const formHTML = await page.locator('.cl-signIn-root').innerHTML();
console.log('[DIAG] sign-in form HTML:', formHTML.substring(0, 3000));
Comment on lines +73 to +74
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Redact auth diagnostics before writing them to CI logs.

formHTML and document.cookie are being logged verbatim here. Those dumps can expose user identifiers, CSRF/session cookies, or other auth state in persisted Actions logs, which is unsafe to merge.

Suggested fix
-        const formHTML = await page.locator('.cl-signIn-root').innerHTML();
-        console.log('[DIAG] sign-in form HTML:', formHTML.substring(0, 3000));
+        const formShape = await page.locator('.cl-signIn-root').evaluate(root => ({
+          inputNames: Array.from(root.querySelectorAll('input')).map(input => input.getAttribute('name')),
+          buttonLabels: Array.from(root.querySelectorAll('button')).map(
+            button => button.textContent?.trim() ?? '',
+          ),
+        }));
+        console.log('[DIAG] sign-in form shape:', JSON.stringify(formShape));

       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,
+          cookieNames: document.cookie
+            .split(';')
+            .map(cookie => cookie.trim().split('=')[0])
+            .filter(Boolean),
           signInCardClass: document.querySelector('.cl-cardBox')?.className ?? 'NOT_FOUND',
         };
       });
@@
         const finalState = await page.evaluate(() => ({
           url: window.location.href,
           hasSession: !!(window as any).Clerk?.session,
-          cookies: document.cookie,
+          cookieNames: document.cookie
+            .split(';')
+            .map(cookie => cookie.trim().split('=')[0])
+            .filter(Boolean),
         }));

Also applies to: 104-117, 123-128

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@integration/tests/cache-components.test.ts` around lines 74 - 75, Logs
currently print sensitive auth data (formHTML and document.cookie). Update the
logging around page.locator('.cl-signIn-root').innerHTML() and any console.log
of document.cookie (and the other instances at the ranges noted) to redact or
mask sensitive fields before writing to CI: truncate large HTML blobs, remove or
replace cookie values and any user identifiers with placeholders (e.g.,
"<REDACTED_COOKIE>" or "<TRUNCATED_HTML>"), and log only non-sensitive
diagnostics such as presence/length or a hashed/partial fingerprint if needed;
ensure all console.log calls referencing formHTML or document.cookie are changed
accordingly.

}

// 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
Expand Down
5 changes: 5 additions & 0 deletions packages/nextjs/src/app-router/client/ClerkProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ const NextClientClerkProvider = <TUi extends Ui = Ui>(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());
}
Expand Down
Loading