@@ -175,3 +175,231 @@ describe('PricingTable - trial info', () => {
175175 } ) ;
176176 } ) ;
177177} ) ;
178+
179+ describe ( 'PricingTable - plans visibility' , ( ) => {
180+ const testPlan = {
181+ id : 'plan_test' ,
182+ name : 'Test Plan' ,
183+ fee : {
184+ amount : 1000 ,
185+ amountFormatted : '10.00' ,
186+ currencySymbol : '$' ,
187+ currency : 'USD' ,
188+ } ,
189+ annualFee : {
190+ amount : 10000 ,
191+ amountFormatted : '100.00' ,
192+ currencySymbol : '$' ,
193+ currency : 'USD' ,
194+ } ,
195+ annualMonthlyFee : {
196+ amount : 833 ,
197+ amountFormatted : '8.33' ,
198+ currencySymbol : '$' ,
199+ currency : 'USD' ,
200+ } ,
201+ description : 'Test plan description' ,
202+ hasBaseFee : true ,
203+ isRecurring : true ,
204+ isDefault : false ,
205+ forPayerType : 'user' ,
206+ publiclyVisible : true ,
207+ slug : 'test' ,
208+ avatarUrl : '' ,
209+ features : [ ] as any [ ] ,
210+ freeTrialEnabled : false ,
211+ freeTrialDays : 0 ,
212+ __internal_toSnapshot : jest . fn ( ) ,
213+ pathRoot : '' ,
214+ reload : jest . fn ( ) ,
215+ } as const ;
216+
217+ it ( 'shows no plans when user is signed in but has no subscription' , async ( ) => {
218+ const { wrapper, fixtures, props } = await createFixtures ( f => {
219+ f . withUser ( { email_addresses : [ 'test@clerk.com' ] } ) ;
220+ } ) ;
221+
222+ // Provide empty props to the PricingTable context
223+ props . setProps ( { } ) ;
224+
225+ fixtures . clerk . billing . getPlans . mockResolvedValue ( { data : [ testPlan as any ] , total_count : 1 } ) ;
226+ // Mock no subscription for signed-in user - empty subscription object
227+ fixtures . clerk . billing . getSubscription . mockResolvedValue ( {
228+ subscriptionItems : [ ] ,
229+ pathRoot : '' ,
230+ reload : jest . fn ( ) ,
231+ } as any ) ;
232+
233+ const { queryByRole } = render ( < PricingTable /> , { wrapper } ) ;
234+
235+ await waitFor ( ( ) => {
236+ // Should not show any plans when signed in but no subscription
237+ expect ( queryByRole ( 'heading' , { name : 'Test Plan' } ) ) . not . toBeInTheDocument ( ) ;
238+ } ) ;
239+ } ) ;
240+
241+ it ( 'shows plans when user is signed in and has a subscription' , async ( ) => {
242+ const { wrapper, fixtures, props } = await createFixtures ( f => {
243+ f . withUser ( { email_addresses : [ 'test@clerk.com' ] } ) ;
244+ } ) ;
245+
246+ // Provide empty props to the PricingTable context
247+ props . setProps ( { } ) ;
248+
249+ fixtures . clerk . billing . getPlans . mockResolvedValue ( { data : [ testPlan as any ] , total_count : 1 } ) ;
250+ // Mock active subscription for signed-in user
251+ fixtures . clerk . billing . getSubscription . mockResolvedValue ( {
252+ id : 'sub_active' ,
253+ status : 'active' ,
254+ activeAt : new Date ( '2021-01-01' ) ,
255+ createdAt : new Date ( '2021-01-01' ) ,
256+ nextPayment : null ,
257+ pastDueAt : null ,
258+ updatedAt : null ,
259+ subscriptionItems : [
260+ {
261+ id : 'si_active' ,
262+ plan : testPlan ,
263+ createdAt : new Date ( '2021-01-01' ) ,
264+ paymentSourceId : 'src_1' ,
265+ pastDueAt : null ,
266+ canceledAt : null ,
267+ periodStart : new Date ( '2021-01-01' ) ,
268+ periodEnd : new Date ( '2021-01-31' ) ,
269+ planPeriod : 'month' as const ,
270+ status : 'active' as const ,
271+ isFreeTrial : false ,
272+ cancel : jest . fn ( ) ,
273+ pathRoot : '' ,
274+ reload : jest . fn ( ) ,
275+ } ,
276+ ] ,
277+ pathRoot : '' ,
278+ reload : jest . fn ( ) ,
279+ } ) ;
280+
281+ const { getByRole } = render ( < PricingTable /> , { wrapper } ) ;
282+
283+ await waitFor ( ( ) => {
284+ // Should show plans when signed in and has subscription
285+ expect ( getByRole ( 'heading' , { name : 'Test Plan' } ) ) . toBeVisible ( ) ;
286+ } ) ;
287+ } ) ;
288+
289+ it ( 'shows plans when user is signed out' , async ( ) => {
290+ const { wrapper, fixtures, props } = await createFixtures ( ) ;
291+
292+ // Provide empty props to the PricingTable context
293+ props . setProps ( { } ) ;
294+
295+ fixtures . clerk . billing . getPlans . mockResolvedValue ( { data : [ testPlan as any ] , total_count : 1 } ) ;
296+ // When signed out, getSubscription should throw or return empty response
297+ fixtures . clerk . billing . getSubscription . mockRejectedValue ( new Error ( 'Unauthenticated' ) ) ;
298+
299+ const { getByRole } = render ( < PricingTable /> , { wrapper } ) ;
300+
301+ await waitFor ( ( ) => {
302+ // Should show plans when signed out
303+ expect ( getByRole ( 'heading' , { name : 'Test Plan' } ) ) . toBeVisible ( ) ;
304+ } ) ;
305+ } ) ;
306+
307+ it ( 'shows no plans when user is signed in but subscription is null' , async ( ) => {
308+ const { wrapper, fixtures, props } = await createFixtures ( f => {
309+ f . withUser ( { email_addresses : [ 'test@clerk.com' ] } ) ;
310+ } ) ;
311+
312+ // Provide empty props to the PricingTable context
313+ props . setProps ( { } ) ;
314+
315+ fixtures . clerk . billing . getPlans . mockResolvedValue ( { data : [ testPlan as any ] , total_count : 1 } ) ;
316+ // Mock null subscription response (different from throwing error)
317+ fixtures . clerk . billing . getSubscription . mockResolvedValue ( null as any ) ;
318+
319+ const { queryByRole } = render ( < PricingTable /> , { wrapper } ) ;
320+
321+ await waitFor ( ( ) => {
322+ // Should not show any plans when signed in but subscription is null
323+ expect ( queryByRole ( 'heading' , { name : 'Test Plan' } ) ) . not . toBeInTheDocument ( ) ;
324+ } ) ;
325+ } ) ;
326+
327+ it ( 'shows no plans when user is signed in but subscription is undefined' , async ( ) => {
328+ const { wrapper, fixtures, props } = await createFixtures ( f => {
329+ f . withUser ( { email_addresses : [ 'test@clerk.com' ] } ) ;
330+ } ) ;
331+
332+ // Provide empty props to the PricingTable context
333+ props . setProps ( { } ) ;
334+
335+ fixtures . clerk . billing . getPlans . mockResolvedValue ( { data : [ testPlan as any ] , total_count : 1 } ) ;
336+ // Mock undefined subscription response (loading state)
337+ fixtures . clerk . billing . getSubscription . mockResolvedValue ( undefined as any ) ;
338+
339+ const { queryByRole } = render ( < PricingTable /> , { wrapper } ) ;
340+
341+ await waitFor ( ( ) => {
342+ // Should not show any plans when signed in but subscription is undefined (loading)
343+ expect ( queryByRole ( 'heading' , { name : 'Test Plan' } ) ) . not . toBeInTheDocument ( ) ;
344+ } ) ;
345+ } ) ;
346+
347+ it ( 'prevents flicker by not showing plans while subscription is loading' , async ( ) => {
348+ const { wrapper, fixtures, props } = await createFixtures ( f => {
349+ f . withUser ( { email_addresses : [ 'test@clerk.com' ] } ) ;
350+ } ) ;
351+
352+ // Provide empty props to the PricingTable context
353+ props . setProps ( { } ) ;
354+
355+ fixtures . clerk . billing . getPlans . mockResolvedValue ( { data : [ testPlan as any ] , total_count : 1 } ) ;
356+
357+ // Create a pending promise and capture its resolver
358+ let resolveSubscription ! : ( value : any ) => void ;
359+ const pendingSubscriptionPromise = new Promise < any > ( resolve => {
360+ resolveSubscription = resolve ;
361+ } ) ;
362+ fixtures . clerk . billing . getSubscription . mockReturnValue ( pendingSubscriptionPromise ) ;
363+
364+ const { queryByRole, findByRole } = render ( < PricingTable /> , { wrapper } ) ;
365+
366+ // Assert no plans render while subscription is pending
367+ await waitFor ( ( ) => {
368+ expect ( queryByRole ( 'heading' , { name : 'Test Plan' } ) ) . not . toBeInTheDocument ( ) ;
369+ } ) ;
370+
371+ // Resolve the subscription with an active subscription object
372+ resolveSubscription ( {
373+ id : 'sub_active' ,
374+ status : 'active' ,
375+ activeAt : new Date ( '2021-01-01' ) ,
376+ createdAt : new Date ( '2021-01-01' ) ,
377+ nextPayment : null ,
378+ pastDueAt : null ,
379+ updatedAt : null ,
380+ subscriptionItems : [
381+ {
382+ id : 'si_active' ,
383+ plan : testPlan ,
384+ createdAt : new Date ( '2021-01-01' ) ,
385+ paymentSourceId : 'src_1' ,
386+ pastDueAt : null ,
387+ canceledAt : null ,
388+ periodStart : new Date ( '2021-01-01' ) ,
389+ periodEnd : new Date ( '2021-01-31' ) ,
390+ planPeriod : 'month' as const ,
391+ status : 'active' as const ,
392+ isFreeTrial : false ,
393+ cancel : jest . fn ( ) ,
394+ pathRoot : '' ,
395+ reload : jest . fn ( ) ,
396+ } ,
397+ ] ,
398+ pathRoot : '' ,
399+ reload : jest . fn ( ) ,
400+ } ) ;
401+
402+ // Assert the plan heading appears after subscription resolves
403+ await findByRole ( 'heading' , { name : 'Test Plan' } ) ;
404+ } ) ;
405+ } ) ;
0 commit comments