Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
dc1a2cd
Implement e2e-tests for transitive state during user and org switching
Ephem Feb 11, 2026
cac0009
Fix transitive state for organizations
Ephem Feb 11, 2026
26ab7dc
Add guard
Ephem Feb 11, 2026
6d4e9fd
Add empty changeset
Ephem Feb 11, 2026
91cd6ab
Bump bundlewatch
Ephem Feb 11, 2026
d593212
Merge branch 'main' into fredrik/user-4666-fix-transitive-state-when-…
jacekradko Feb 11, 2026
bc47cd0
Implement failing test
Ephem Feb 12, 2026
68c42f2
Fix signOut transitive state by moving it before session removal
Ephem Feb 12, 2026
a0cb577
Merge remote-tracking branch 'origin/main' into fredrik/user-4666-fix…
jacekradko Feb 13, 2026
e766ddd
Merge branch 'main' into fredrik/user-4666-fix-transitive-state-when-…
Ephem Feb 16, 2026
dacad67
Fix flaky test
Ephem Feb 16, 2026
9b1a9f0
Update toBeSignedOut helper to assert only for null
Ephem Feb 16, 2026
2895806
Fix broken localhost test
Ephem Feb 16, 2026
786c0f2
Fix another broken localhost test
Ephem Feb 16, 2026
2e55f09
Update changeset to include toBeSignedOut test-helper changes
Ephem Feb 16, 2026
df92278
Extract getClientResourceFromPayload to helper again
Ephem Feb 16, 2026
51c0123
Consolidate fetch across touch and internal touch
Ephem Feb 16, 2026
c910e44
Add test mocks for Session.__internal_touch
Ephem Feb 16, 2026
a3e702a
Type Session.__internal_touch
Ephem Feb 17, 2026
25cc751
Try to fix flaky impersonation test
Ephem Feb 17, 2026
d1ff078
Merge branch 'main' into fredrik/user-4666-fix-transitive-state-when-…
Ephem Feb 17, 2026
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
6 changes: 6 additions & 0 deletions .changeset/olive-oranges-march.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/clerk-js': patch
'@clerk/testing': patch
---

Fix `toBeSignedOut` test-helper so it only resolves when `user === null`. It previously resolved for any falsy value, which could give false positives when Clerk had not loaded yet, or during auth-state changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default async function Page({ params }: { params: Promise<{ orgId: string }> }) {
const { orgId } = await params;

return (
<div>
<p data-testid='current-org-id'>{orgId}</p>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use client';

import { OrganizationSwitcher, useAuth } from '@clerk/nextjs';
import { useState } from 'react';
import { usePathname } from 'next/navigation';

function EmissionLog() {
const { orgId } = useAuth();
const pathname = usePathname();
const [log, setLog] = useState<string[]>([]);

const entry = `${pathname} - ${orgId}`;
if (entry !== log[log.length - 1]) {
setLog(prev => [...prev, entry]);
}

return (
<ul data-testid='emission-log'>
{log.map((entry, i) => (
<li
key={i}
data-testid={`emission-${i}`}
>
{entry}
</li>
))}
</ul>
);
}

export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div style={{ margin: '40px' }}>
<div style={{ marginBottom: '20px' }}>
<OrganizationSwitcher
fallback={<div>Loading organization switcher</div>}
afterSelectOrganizationUrl='/transitive-state/organization-switcher/:id'
/>
</div>
<div style={{ marginBottom: '20px' }}>
<h2>Emission log</h2>
<EmissionLog />
</div>
{children}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client';

import { useAuth } from '@clerk/nextjs';
import { useState } from 'react';
import { usePathname } from 'next/navigation';

function EmissionLog() {
const { userId } = useAuth();
const pathname = usePathname();
const [log, setLog] = useState<string[]>([]);

const entry = `${pathname} - ${String(userId)}`;
if (entry !== log[log.length - 1]) {
setLog(prev => [...prev, entry]);
}

return (
<ul data-testid='emission-log'>
{log.map((entry, i) => (
<li
key={i}
data-testid={`emission-${i}`}
>
{entry}
</li>
))}
</ul>
);
}

export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div style={{ margin: '40px' }}>
<div style={{ marginBottom: '20px' }}>
<h2>Emission log</h2>
<EmissionLog />
</div>
{children}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { SignOutButton } from '@clerk/nextjs';

export default function Page() {
return (
<div>
<p data-testid='page-name'>sign-out</p>
<SignOutButton redirectUrl='/transitive-state/sign-out/sign-in' />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <p data-testid='page-name'>sign-in</p>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use client';

import { UserButton, useAuth } from '@clerk/nextjs';
import { useState } from 'react';
import { usePathname } from 'next/navigation';

function EmissionLog() {
const { userId } = useAuth();
const pathname = usePathname();
const [log, setLog] = useState<string[]>([]);

const entry = `${pathname} - ${userId}`;
if (entry !== log[log.length - 1]) {
setLog(prev => [...prev, entry]);
}

return (
<ul data-testid='emission-log'>
{log.map((entry, i) => (
<li
key={i}
data-testid={`emission-${i}`}
>
{entry}
</li>
))}
</ul>
);
}

export default function Layout({ children }: { children: React.ReactNode }) {
return (
<div style={{ margin: '40px' }}>
<div style={{ marginBottom: '20px' }}>
<UserButton
fallback={<div>Loading user button</div>}
afterSwitchSessionUrl='/transitive-state/user-button/switched'
/>
</div>
<div style={{ marginBottom: '20px' }}>
<h2>Emission log</h2>
<EmissionLog />
</div>
{children}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <p data-testid='page-name'>initial</p>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return <p data-testid='page-name'>switched</p>;
}
4 changes: 3 additions & 1 deletion integration/tests/impersonation-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('Impersona
// Pass through the ticket flow
const searchParams = new URLSearchParams();
searchParams.set('__clerk_ticket', actorTokenResponse.token);
await u.po.signIn.goTo({ searchParams });
// We don't use u.signIn.goTo here since the navigation can happen so quickly
// that Playwright can miss catching the sign in component having been mounted
await u.page.goToRelative('/sign-in', { searchParams });

// Ensure that the impersonation flow is successful
await u.po.expect.toBeSignedInAsActor();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ test.describe('multiple apps running on localhost using different Clerk instance
expect(tab0Cookies.filter(c => c.name.startsWith('__clerk_db_jwt'))).toHaveLength(2);
expect(tab0Cookies.filter(c => c.name.startsWith('__client_uat'))).toHaveLength(2);

await u[1].po.expect.toBeSignedOut();
await u[1].po.signIn.goTo();
await u[1].po.expect.toBeSignedOut();
await u[1].po.signIn.signInWithEmailAndInstantPassword(fakeUsers[1]);
await u[1].po.expect.toBeSignedIn();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ test.describe('multiple apps running on localhost using same Clerk instance @loc

// sign out from tab1
await u[1].page.goToAppHome();
// This also ensures Clerk has loaded before evaluating the signOut
await u[1].po.expect.toBeSignedIn();
await u[1].page.evaluate(() => window.Clerk.signOut());
await u[1].po.expect.toBeSignedOut();

Expand Down
1 change: 1 addition & 0 deletions integration/tests/sign-out-smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign out
await u.page.getByRole('button', { name: 'Open user menu' }).click();

await u.page.getByRole('menuitem', { name: 'Sign out' }).click();
await u.po.expect.toBeSignedOut();
await u.page.getByRole('link', { name: 'Protected', exact: true }).click();
await u.page.waitForURL(url => url.href.includes('/sign-in?redirect_url'));
});
Expand Down
Loading
Loading