Skip to content
Open
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
266 changes: 266 additions & 0 deletions packages/clerk-js/src/core/__tests__/state.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { eventBus } from '../events';
import { SignIn } from '../resources/SignIn';
import { SignUp } from '../resources/SignUp';
import { signInResourceSignal, signUpResourceSignal } from '../signals';
import { State } from '../state';

describe('State', () => {
let state: State;

Check failure on line 10 in packages/clerk-js/src/core/__tests__/state.test.ts

View workflow job for this annotation

GitHub Actions / Static analysis

'state' is assigned a value but never used. Allowed unused vars must match /^_/u

// Capture original static clerk references to restore after tests
const originalSignUpClerk = SignUp.clerk;
const originalSignInClerk = SignIn.clerk;

beforeEach(() => {
// Reset signals to initial state
signUpResourceSignal({ resource: null });
signInResourceSignal({ resource: null });
// Create a new State instance which registers event handlers
state = new State();
});

afterEach(() => {
vi.clearAllMocks();
// Reset signals after each test
signUpResourceSignal({ resource: null });
signInResourceSignal({ resource: null });
// Restore original clerk references to prevent global state leakage
SignUp.clerk = originalSignUpClerk;
SignIn.clerk = originalSignInClerk;
});

describe('shouldIgnoreNullUpdate behavior', () => {
describe('SignUp', () => {
it('should allow first resource update when previous resource is null', () => {
// Arrange: Signal starts with null
expect(signUpResourceSignal().resource).toBeNull();

// Act: Emit a resource update with a SignUp that has an id
const signUp = new SignUp({ id: 'signup_123', status: 'missing_requirements' } as any);

// Assert: Signal should be updated
expect(signUpResourceSignal().resource).toBe(signUp);
expect(signUpResourceSignal().resource?.id).toBe('signup_123');
});

it('should ignore null resource update when previous resource exists and canBeDiscarded is false', () => {
// Arrange: Set up a SignUp with id and canBeDiscarded = false (default)
const existingSignUp = new SignUp({ id: 'signup_123', status: 'missing_requirements' } as any);
expect(signUpResourceSignal().resource).toBe(existingSignUp);
expect(existingSignUp.__internal_future.canBeDiscarded).toBe(false);

// Act: Emit a resource update with a null SignUp (simulating client refresh with null sign_up)
const nullSignUp = new SignUp(null);

Check failure on line 55 in packages/clerk-js/src/core/__tests__/state.test.ts

View workflow job for this annotation

GitHub Actions / Static analysis

'nullSignUp' is assigned a value but never used. Allowed unused vars must match /^_/u

// Assert: Signal should NOT be updated - should still have the existing SignUp
expect(signUpResourceSignal().resource).toBe(existingSignUp);
expect(signUpResourceSignal().resource?.id).toBe('signup_123');
});

it('should allow null resource update when previous resource exists and canBeDiscarded is true', async () => {
// Arrange: Set up a SignUp with id and mock setActive
const mockSetActive = vi.fn().mockResolvedValue({});
SignUp.clerk = { setActive: mockSetActive } as any;

const existingSignUp = new SignUp({
id: 'signup_123',
status: 'complete',
created_session_id: 'session_123',
} as any);
expect(signUpResourceSignal().resource).toBe(existingSignUp);
expect(existingSignUp.__internal_future.canBeDiscarded).toBe(false);

// Act: Call finalize() which sets canBeDiscarded to true
await existingSignUp.__internal_future.finalize();

// Verify canBeDiscarded is now true
expect(existingSignUp.__internal_future.canBeDiscarded).toBe(true);
expect(mockSetActive).toHaveBeenCalledWith({ session: 'session_123', navigate: undefined });

// Now emit a null resource update (simulating client refresh with null sign_up)
const nullSignUp = new SignUp(null);

// Assert: The null update SHOULD be allowed because canBeDiscarded is true
// shouldIgnoreNullUpdate returns false when canBeDiscarded is true
expect(signUpResourceSignal().resource).toBe(nullSignUp);
expect(signUpResourceSignal().resource?.id).toBeUndefined();
});

it('should allow null resource update after reset() is called', async () => {
// Arrange: Set up mock client that tracks the new SignUp created during reset
let newSignUpFromReset: SignUp | null = null;
const mockClient = {
signUp: new SignUp(null),
resetSignUp: vi.fn().mockImplementation(function (this: typeof mockClient) {
newSignUpFromReset = new SignUp(null);
this.signUp = newSignUpFromReset;
// reset() emits resource:error to clear errors, but the signal update
// happens via resource:update when the new SignUp is created
eventBus.emit('resource:error', { resource: newSignUpFromReset, error: null });
// Emit resource:update to update the signal (simulating what happens in real flow)
eventBus.emit('resource:update', { resource: newSignUpFromReset });
}),
};
SignUp.clerk = { client: mockClient } as any;

// Create a SignUp with id
const existingSignUp = new SignUp({ id: 'signup_123', status: 'missing_requirements' } as any);
expect(signUpResourceSignal().resource?.id).toBe('signup_123');
expect(existingSignUp.__internal_future.canBeDiscarded).toBe(false);

// Act: Call reset() - this sets canBeDiscarded to true before resetting
await existingSignUp.__internal_future.reset();

// Assert: Verify reset was called
expect(mockClient.resetSignUp).toHaveBeenCalled();

// Assert: Verify canBeDiscarded was set to true on the original SignUp
expect(existingSignUp.__internal_future.canBeDiscarded).toBe(true);

// Assert: Verify the signal was updated with a new SignUp that has no id
// The previous id 'signup_123' should be gone
expect(signUpResourceSignal().resource).toBe(newSignUpFromReset);
expect(signUpResourceSignal().resource?.id).toBeUndefined();
expect(signUpResourceSignal().resource).not.toBe(existingSignUp);
});

it('should allow resource update when new resource has an id (not a null update)', () => {
// Arrange: Set up a SignUp with id
const existingSignUp = new SignUp({ id: 'signup_123', status: 'missing_requirements' } as any);

Check failure on line 131 in packages/clerk-js/src/core/__tests__/state.test.ts

View workflow job for this annotation

GitHub Actions / Static analysis

'existingSignUp' is assigned a value but never used. Allowed unused vars must match /^_/u
expect(signUpResourceSignal().resource?.id).toBe('signup_123');

// Act: Emit a resource update with a different SignUp that also has an id
const newSignUp = new SignUp({ id: 'signup_456', status: 'complete' } as any);

// Assert: Signal should be updated with the new SignUp
expect(signUpResourceSignal().resource).toBe(newSignUp);
expect(signUpResourceSignal().resource?.id).toBe('signup_456');
});
});

describe('SignIn', () => {
it('should allow first resource update when previous resource is null', () => {
// Arrange: Signal starts with null
expect(signInResourceSignal().resource).toBeNull();

// Act: Emit a resource update with a SignIn that has an id
const signIn = new SignIn({ id: 'signin_123', status: 'needs_identifier' } as any);

// Assert: Signal should be updated
expect(signInResourceSignal().resource).toBe(signIn);
expect(signInResourceSignal().resource?.id).toBe('signin_123');
});

it('should ignore null resource update when previous resource exists and canBeDiscarded is false', () => {
// Arrange: Set up a SignIn with id and canBeDiscarded = false (default)
const existingSignIn = new SignIn({ id: 'signin_123', status: 'needs_identifier' } as any);
expect(signInResourceSignal().resource).toBe(existingSignIn);
expect(existingSignIn.__internal_future.canBeDiscarded).toBe(false);

// Act: Emit a resource update with a null SignIn (simulating client refresh with null sign_in)
const nullSignIn = new SignIn(null);

Check failure on line 163 in packages/clerk-js/src/core/__tests__/state.test.ts

View workflow job for this annotation

GitHub Actions / Static analysis

'nullSignIn' is assigned a value but never used. Allowed unused vars must match /^_/u

// Assert: Signal should NOT be updated - should still have the existing SignIn
expect(signInResourceSignal().resource).toBe(existingSignIn);
expect(signInResourceSignal().resource?.id).toBe('signin_123');
});
});
});

describe('Simulated client refresh scenarios', () => {
it('should protect against null update during email verification (before finalize)', () => {
// This test simulates the scenario described in USER-4372:
// 1. User is in the middle of sign up with verified email
// 2. Client refresh happens (e.g., router.refresh in Next.js)
// 3. Server returns sign_up: null because sign up is complete on server side
// 4. The null update should be IGNORED to prevent flash of default form

// Arrange: User has completed email verification but hasn't called finalize yet
const signUp = new SignUp({
id: 'signup_123',
status: 'complete',
created_session_id: 'session_123',
verifications: {
email_address: { status: 'verified' },
},
} as any);

expect(signUpResourceSignal().resource?.id).toBe('signup_123');
expect(signUp.__internal_future.canBeDiscarded).toBe(false);

// Act: Simulate client refresh returning null sign_up
// This is what happens in Client.fromJSON when server returns sign_up: null
const nullSignUp = new SignUp(null);

Check failure on line 195 in packages/clerk-js/src/core/__tests__/state.test.ts

View workflow job for this annotation

GitHub Actions / Static analysis

'nullSignUp' is assigned a value but never used. Allowed unused vars must match /^_/u

// Assert: The null update should be IGNORED
expect(signUpResourceSignal().resource?.id).toBe('signup_123');
expect(signUpResourceSignal().resource).toBe(signUp);
});

it('should allow null update after finalize sets canBeDiscarded to true', async () => {
// This test verifies that after finalize() is called,
// the null update IS allowed (because canBeDiscarded is true)

// Arrange
const mockSetActive = vi.fn().mockResolvedValue({});
SignUp.clerk = { setActive: mockSetActive } as any;

const signUp = new SignUp({
id: 'signup_123',
status: 'complete',
created_session_id: 'session_123',
} as any);

expect(signUpResourceSignal().resource?.id).toBe('signup_123');
expect(signUp.__internal_future.canBeDiscarded).toBe(false);

// Act: Call finalize - this sets canBeDiscarded to true
await signUp.__internal_future.finalize();

// Verify canBeDiscarded is now true
expect(signUp.__internal_future.canBeDiscarded).toBe(true);

// Now simulate a null update
const nullSignUp = new SignUp(null);

// Assert: The null update SHOULD be allowed now
// Because canBeDiscarded is true, shouldIgnoreNullUpdate returns false
expect(signUpResourceSignal().resource).toBe(nullSignUp);
expect(signUpResourceSignal().resource?.id).toBeUndefined();
});
});

describe('Edge cases', () => {
it('should handle rapid successive updates correctly', () => {
// First update with valid SignUp
const signUp1 = new SignUp({ id: 'signup_1', status: 'missing_requirements' } as any);

Check failure on line 238 in packages/clerk-js/src/core/__tests__/state.test.ts

View workflow job for this annotation

GitHub Actions / Static analysis

'signUp1' is assigned a value but never used. Allowed unused vars must match /^_/u
expect(signUpResourceSignal().resource?.id).toBe('signup_1');

// Second update with another valid SignUp
const signUp2 = new SignUp({ id: 'signup_2', status: 'missing_requirements' } as any);

Check failure on line 242 in packages/clerk-js/src/core/__tests__/state.test.ts

View workflow job for this annotation

GitHub Actions / Static analysis

'signUp2' is assigned a value but never used. Allowed unused vars must match /^_/u
expect(signUpResourceSignal().resource?.id).toBe('signup_2');

// Null update should be ignored
const nullSignUp = new SignUp(null);

Check failure on line 246 in packages/clerk-js/src/core/__tests__/state.test.ts

View workflow job for this annotation

GitHub Actions / Static analysis

'nullSignUp' is assigned a value but never used. Allowed unused vars must match /^_/u
expect(signUpResourceSignal().resource?.id).toBe('signup_2');

// Another valid update should work
const signUp3 = new SignUp({ id: 'signup_3', status: 'complete' } as any);

Check failure on line 250 in packages/clerk-js/src/core/__tests__/state.test.ts

View workflow job for this annotation

GitHub Actions / Static analysis

'signUp3' is assigned a value but never used. Allowed unused vars must match /^_/u
expect(signUpResourceSignal().resource?.id).toBe('signup_3');
});

it('should handle update with same instance correctly', () => {
// Create a SignUp
const signUp = new SignUp({ id: 'signup_123', status: 'missing_requirements' } as any);
expect(signUpResourceSignal().resource?.id).toBe('signup_123');

// Manually emit update with the same instance (simulating fromJSON on same instance)
eventBus.emit('resource:update', { resource: signUp });

// Signal should still have the same instance
expect(signUpResourceSignal().resource).toBe(signUp);
});
});
});
Loading