Skip to content

Commit f419da1

Browse files
committed
test(clerk-js): add tests for null resource update behavior
1 parent 79bbbed commit f419da1

1 file changed

Lines changed: 271 additions & 0 deletions

File tree

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2+
3+
import { eventBus } from '../events';
4+
import { SignIn } from '../resources/SignIn';
5+
import { SignUp } from '../resources/SignUp';
6+
import { signInResourceSignal, signUpResourceSignal } from '../signals';
7+
import { State } from '../state';
8+
9+
describe('State', () => {
10+
let state: State;
11+
12+
// Capture original static clerk references to restore after tests
13+
const originalSignUpClerk = SignUp.clerk;
14+
const originalSignInClerk = SignIn.clerk;
15+
16+
beforeEach(() => {
17+
// Reset signals to initial state
18+
signUpResourceSignal({ resource: null });
19+
signInResourceSignal({ resource: null });
20+
// Create a new State instance which registers event handlers
21+
state = new State();
22+
});
23+
24+
afterEach(() => {
25+
vi.clearAllMocks();
26+
// Reset signals after each test
27+
signUpResourceSignal({ resource: null });
28+
signInResourceSignal({ resource: null });
29+
// Restore original clerk references to prevent global state leakage
30+
SignUp.clerk = originalSignUpClerk;
31+
SignIn.clerk = originalSignInClerk;
32+
});
33+
34+
describe('shouldIgnoreNullUpdate behavior', () => {
35+
describe('SignUp', () => {
36+
it('should allow first resource update when previous resource is null', () => {
37+
// Arrange: Signal starts with null
38+
expect(signUpResourceSignal().resource).toBeNull();
39+
40+
// Act: Emit a resource update with a SignUp that has an id
41+
const signUp = new SignUp({ id: 'signup_123', status: 'missing_requirements' } as any);
42+
43+
// Assert: Signal should be updated
44+
expect(signUpResourceSignal().resource).toBe(signUp);
45+
expect(signUpResourceSignal().resource?.id).toBe('signup_123');
46+
});
47+
48+
it('should ignore null resource update when previous resource exists and canBeDiscarded is false', () => {
49+
// Arrange: Set up a SignUp with id and canBeDiscarded = false (default)
50+
const existingSignUp = new SignUp({ id: 'signup_123', status: 'missing_requirements' } as any);
51+
expect(signUpResourceSignal().resource).toBe(existingSignUp);
52+
expect(existingSignUp.__internal_future.canBeDiscarded).toBe(false);
53+
54+
// Act: Emit a resource update with a null SignUp (simulating client refresh with null sign_up)
55+
const nullSignUp = new SignUp(null);
56+
57+
// Assert: Signal should NOT be updated - should still have the existing SignUp
58+
expect(signUpResourceSignal().resource).toBe(existingSignUp);
59+
expect(signUpResourceSignal().resource?.id).toBe('signup_123');
60+
});
61+
62+
it('should allow null resource update when previous resource exists and canBeDiscarded is true', async () => {
63+
// Arrange: Set up a SignUp with id and mock setActive
64+
const mockSetActive = vi.fn().mockResolvedValue({});
65+
SignUp.clerk = { setActive: mockSetActive } as any;
66+
67+
const existingSignUp = new SignUp({
68+
id: 'signup_123',
69+
status: 'complete',
70+
created_session_id: 'session_123',
71+
} as any);
72+
expect(signUpResourceSignal().resource).toBe(existingSignUp);
73+
expect(existingSignUp.__internal_future.canBeDiscarded).toBe(false);
74+
75+
// Act: Call finalize() which sets canBeDiscarded to true
76+
await existingSignUp.__internal_future.finalize();
77+
78+
// Verify canBeDiscarded is now true
79+
expect(existingSignUp.__internal_future.canBeDiscarded).toBe(true);
80+
expect(mockSetActive).toHaveBeenCalledWith({ session: 'session_123', navigate: undefined });
81+
82+
// Now emit a null resource update (simulating client refresh with null sign_up)
83+
const nullSignUp = new SignUp(null);
84+
85+
// Assert: The null update SHOULD be allowed because canBeDiscarded is true
86+
// shouldIgnoreNullUpdate returns false when canBeDiscarded is true
87+
expect(signUpResourceSignal().resource).toBe(nullSignUp);
88+
expect(signUpResourceSignal().resource?.id).toBeUndefined();
89+
});
90+
91+
it('should allow null resource update after reset() is called', async () => {
92+
// Arrange: Set up mock client that tracks the new SignUp created during reset
93+
let newSignUpFromReset: SignUp | null = null;
94+
const mockClient = {
95+
signUp: new SignUp(null),
96+
resetSignUp: vi.fn().mockImplementation(function (this: typeof mockClient) {
97+
newSignUpFromReset = new SignUp(null);
98+
this.signUp = newSignUpFromReset;
99+
// reset() emits resource:error to clear errors, but the signal update
100+
// happens via resource:update when the new SignUp is created
101+
eventBus.emit('resource:error', { resource: newSignUpFromReset, error: null });
102+
// Emit resource:update to update the signal (simulating what happens in real flow)
103+
eventBus.emit('resource:update', { resource: newSignUpFromReset });
104+
}),
105+
};
106+
SignUp.clerk = { client: mockClient } as any;
107+
108+
// Create a SignUp with id
109+
const existingSignUp = new SignUp({ id: 'signup_123', status: 'missing_requirements' } as any);
110+
expect(signUpResourceSignal().resource?.id).toBe('signup_123');
111+
expect(existingSignUp.__internal_future.canBeDiscarded).toBe(false);
112+
113+
// Act: Call reset() - this sets canBeDiscarded to true before resetting
114+
await existingSignUp.__internal_future.reset();
115+
116+
// Assert: Verify reset was called
117+
expect(mockClient.resetSignUp).toHaveBeenCalled();
118+
119+
// Assert: Verify canBeDiscarded was set to true on the original SignUp
120+
expect(existingSignUp.__internal_future.canBeDiscarded).toBe(true);
121+
122+
// Assert: Verify the signal was updated with a new SignUp that has no id
123+
// The previous id 'signup_123' should be gone
124+
expect(signUpResourceSignal().resource).toBe(newSignUpFromReset);
125+
expect(signUpResourceSignal().resource?.id).toBeUndefined();
126+
});
127+
128+
it('should allow resource update when new resource has an id (not a null update)', () => {
129+
// Arrange: Set up a SignUp with id
130+
const existingSignUp = new SignUp({ id: 'signup_123', status: 'missing_requirements' } as any);
131+
expect(signUpResourceSignal().resource?.id).toBe('signup_123');
132+
133+
// Act: Emit a resource update with a different SignUp that also has an id
134+
const newSignUp = new SignUp({ id: 'signup_456', status: 'complete' } as any);
135+
136+
// Assert: Signal should be updated with the new SignUp
137+
expect(signUpResourceSignal().resource).toBe(newSignUp);
138+
expect(signUpResourceSignal().resource?.id).toBe('signup_456');
139+
});
140+
});
141+
142+
describe('SignIn', () => {
143+
it('should allow first resource update when previous resource is null', () => {
144+
// Arrange: Signal starts with null
145+
expect(signInResourceSignal().resource).toBeNull();
146+
147+
// Act: Emit a resource update with a SignIn that has an id
148+
const signIn = new SignIn({ id: 'signin_123', status: 'needs_identifier' } as any);
149+
150+
// Assert: Signal should be updated
151+
expect(signInResourceSignal().resource).toBe(signIn);
152+
expect(signInResourceSignal().resource?.id).toBe('signin_123');
153+
});
154+
155+
it('should ignore null resource update when previous resource exists and canBeDiscarded is false', () => {
156+
// Arrange: Set up a SignIn with id and canBeDiscarded = false (default)
157+
const existingSignIn = new SignIn({ id: 'signin_123', status: 'needs_identifier' } as any);
158+
expect(signInResourceSignal().resource).toBe(existingSignIn);
159+
expect(existingSignIn.__internal_future.canBeDiscarded).toBe(false);
160+
161+
// Act: Emit a resource update with a null SignIn (simulating client refresh with null sign_in)
162+
const nullSignIn = new SignIn(null);
163+
164+
// Assert: Signal should NOT be updated - should still have the existing SignIn
165+
expect(signInResourceSignal().resource).toBe(existingSignIn);
166+
expect(signInResourceSignal().resource?.id).toBe('signin_123');
167+
});
168+
169+
it('should allow null resource update when previous resource exists and canBeDiscarded is true', async () => {
170+
// Arrange: Set up a SignIn with id and mock setActive
171+
const mockSetActive = vi.fn().mockResolvedValue({});
172+
SignIn.clerk = { setActive: mockSetActive, client: { sessions: [{ id: 'session_123' }] } } as any;
173+
174+
const existingSignIn = new SignIn({
175+
id: 'signin_123',
176+
status: 'complete',
177+
created_session_id: 'session_123',
178+
} as any);
179+
expect(signInResourceSignal().resource).toBe(existingSignIn);
180+
expect(existingSignIn.__internal_future.canBeDiscarded).toBe(false);
181+
182+
// Act: Call finalize() which sets canBeDiscarded to true
183+
await existingSignIn.__internal_future.finalize();
184+
185+
expect(existingSignIn.__internal_future.canBeDiscarded).toBe(true);
186+
expect(mockSetActive).toHaveBeenCalledWith({ session: 'session_123', navigate: undefined });
187+
188+
// Now emit a null resource update
189+
const nullSignIn = new SignIn(null);
190+
191+
// Assert: The null update SHOULD be allowed because canBeDiscarded is true
192+
expect(signInResourceSignal().resource).toBe(nullSignIn);
193+
expect(signInResourceSignal().resource?.id).toBeUndefined();
194+
});
195+
196+
it('should allow null resource update after reset() is called', async () => {
197+
// Arrange: Set up mock client
198+
let newSignInFromReset: SignIn | null = null;
199+
const mockClient = {
200+
signIn: new SignIn(null),
201+
resetSignIn: vi.fn().mockImplementation(function (this: typeof mockClient) {
202+
newSignInFromReset = new SignIn(null);
203+
this.signIn = newSignInFromReset;
204+
eventBus.emit('resource:error', { resource: newSignInFromReset, error: null });
205+
eventBus.emit('resource:update', { resource: newSignInFromReset });
206+
}),
207+
};
208+
SignIn.clerk = { client: mockClient } as any;
209+
210+
// Create a SignIn with id
211+
const existingSignIn = new SignIn({ id: 'signin_123', status: 'needs_identifier' } as any);
212+
expect(signInResourceSignal().resource?.id).toBe('signin_123');
213+
expect(existingSignIn.__internal_future.canBeDiscarded).toBe(false);
214+
215+
// Act: Call reset()
216+
await existingSignIn.__internal_future.reset();
217+
218+
// Assert
219+
expect(mockClient.resetSignIn).toHaveBeenCalled();
220+
expect(existingSignIn.__internal_future.canBeDiscarded).toBe(true);
221+
expect(signInResourceSignal().resource).toBe(newSignInFromReset);
222+
expect(signInResourceSignal().resource?.id).toBeUndefined();
223+
});
224+
225+
it('should allow resource update when new resource has an id (not a null update)', () => {
226+
// Arrange: Set up a SignIn with id
227+
const existingSignIn = new SignIn({ id: 'signin_123', status: 'needs_identifier' } as any);
228+
expect(signInResourceSignal().resource?.id).toBe('signin_123');
229+
230+
// Act: Emit a resource update with a different SignIn that also has an id
231+
const newSignIn = new SignIn({ id: 'signin_456', status: 'complete' } as any);
232+
233+
// Assert: Signal should be updated with the new SignIn
234+
expect(signInResourceSignal().resource).toBe(newSignIn);
235+
expect(signInResourceSignal().resource?.id).toBe('signin_456');
236+
});
237+
});
238+
});
239+
240+
describe('Edge cases', () => {
241+
it('should handle rapid successive updates correctly', () => {
242+
// First update with valid SignUp
243+
const signUp1 = new SignUp({ id: 'signup_1', status: 'missing_requirements' } as any);
244+
expect(signUpResourceSignal().resource?.id).toBe('signup_1');
245+
246+
// Second update with another valid SignUp
247+
const signUp2 = new SignUp({ id: 'signup_2', status: 'missing_requirements' } as any);
248+
expect(signUpResourceSignal().resource?.id).toBe('signup_2');
249+
250+
// Null update should be ignored
251+
const nullSignUp = new SignUp(null);
252+
expect(signUpResourceSignal().resource?.id).toBe('signup_2');
253+
254+
// Another valid update should work
255+
const signUp3 = new SignUp({ id: 'signup_3', status: 'complete' } as any);
256+
expect(signUpResourceSignal().resource?.id).toBe('signup_3');
257+
});
258+
259+
it('should handle update with same instance correctly', () => {
260+
// Create a SignUp
261+
const signUp = new SignUp({ id: 'signup_123', status: 'missing_requirements' } as any);
262+
expect(signUpResourceSignal().resource?.id).toBe('signup_123');
263+
264+
// Manually emit update with the same instance (simulating fromJSON on same instance)
265+
eventBus.emit('resource:update', { resource: signUp });
266+
267+
// Signal should still have the same instance
268+
expect(signUpResourceSignal().resource).toBe(signUp);
269+
});
270+
});
271+
});

0 commit comments

Comments
 (0)