-
Notifications
You must be signed in to change notification settings - Fork 435
test(clerk-js): add tests for null resource update behavior #7753
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
jacekradko
merged 2 commits into
main
from
jacek/user-4372-verify-null-resource-updates-when-previous-resource-is-not
Feb 11, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| --- | ||
| --- |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,271 @@ | ||
| 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; | ||
|
|
||
| // 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); | ||
|
|
||
| // 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(); | ||
| }); | ||
|
|
||
| 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); | ||
| 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); | ||
|
|
||
| // Assert: Signal should NOT be updated - should still have the existing SignIn | ||
| expect(signInResourceSignal().resource).toBe(existingSignIn); | ||
| expect(signInResourceSignal().resource?.id).toBe('signin_123'); | ||
| }); | ||
|
|
||
| it('should allow null resource update when previous resource exists and canBeDiscarded is true', async () => { | ||
| // Arrange: Set up a SignIn with id and mock setActive | ||
| const mockSetActive = vi.fn().mockResolvedValue({}); | ||
| SignIn.clerk = { setActive: mockSetActive, client: { sessions: [{ id: 'session_123' }] } } as any; | ||
|
|
||
| const existingSignIn = new SignIn({ | ||
| id: 'signin_123', | ||
| status: 'complete', | ||
| created_session_id: 'session_123', | ||
| } as any); | ||
| expect(signInResourceSignal().resource).toBe(existingSignIn); | ||
| expect(existingSignIn.__internal_future.canBeDiscarded).toBe(false); | ||
|
|
||
| // Act: Call finalize() which sets canBeDiscarded to true | ||
| await existingSignIn.__internal_future.finalize(); | ||
|
|
||
| expect(existingSignIn.__internal_future.canBeDiscarded).toBe(true); | ||
| expect(mockSetActive).toHaveBeenCalledWith({ session: 'session_123', navigate: undefined }); | ||
|
|
||
| // Now emit a null resource update | ||
| const nullSignIn = new SignIn(null); | ||
|
|
||
| // Assert: The null update SHOULD be allowed because canBeDiscarded is true | ||
| expect(signInResourceSignal().resource).toBe(nullSignIn); | ||
| expect(signInResourceSignal().resource?.id).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('should allow null resource update after reset() is called', async () => { | ||
| // Arrange: Set up mock client | ||
| let newSignInFromReset: SignIn | null = null; | ||
| const mockClient = { | ||
| signIn: new SignIn(null), | ||
| resetSignIn: vi.fn().mockImplementation(function (this: typeof mockClient) { | ||
| newSignInFromReset = new SignIn(null); | ||
| this.signIn = newSignInFromReset; | ||
| eventBus.emit('resource:error', { resource: newSignInFromReset, error: null }); | ||
| eventBus.emit('resource:update', { resource: newSignInFromReset }); | ||
| }), | ||
| }; | ||
| SignIn.clerk = { client: mockClient } as any; | ||
|
|
||
| // Create a SignIn with id | ||
| const existingSignIn = new SignIn({ id: 'signin_123', status: 'needs_identifier' } as any); | ||
| expect(signInResourceSignal().resource?.id).toBe('signin_123'); | ||
| expect(existingSignIn.__internal_future.canBeDiscarded).toBe(false); | ||
|
|
||
| // Act: Call reset() | ||
| await existingSignIn.__internal_future.reset(); | ||
|
|
||
| // Assert | ||
| expect(mockClient.resetSignIn).toHaveBeenCalled(); | ||
| expect(existingSignIn.__internal_future.canBeDiscarded).toBe(true); | ||
| expect(signInResourceSignal().resource).toBe(newSignInFromReset); | ||
| expect(signInResourceSignal().resource?.id).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('should allow resource update when new resource has an id (not a null update)', () => { | ||
| // Arrange: Set up a SignIn with id | ||
| const _existingSignIn = new SignIn({ id: 'signin_123', status: 'needs_identifier' } as any); | ||
| expect(signInResourceSignal().resource?.id).toBe('signin_123'); | ||
|
|
||
| // Act: Emit a resource update with a different SignIn that also has an id | ||
| const newSignIn = new SignIn({ id: 'signin_456', status: 'complete' } as any); | ||
|
|
||
| // Assert: Signal should be updated with the new SignIn | ||
| expect(signInResourceSignal().resource).toBe(newSignIn); | ||
| expect(signInResourceSignal().resource?.id).toBe('signin_456'); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| 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); | ||
| 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); | ||
| expect(signUpResourceSignal().resource?.id).toBe('signup_2'); | ||
|
|
||
| // Null update should be ignored | ||
| const _nullSignUp = new SignUp(null); | ||
| expect(signUpResourceSignal().resource?.id).toBe('signup_2'); | ||
|
|
||
| // Another valid update should work | ||
| const _signUp3 = new SignUp({ id: 'signup_3', status: 'complete' } as any); | ||
| 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); | ||
| }); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.