From ea53d8d70e217c43057f04e1413172206606eb6c Mon Sep 17 00:00:00 2001 From: imeepos Date: Wed, 28 Jan 2026 14:46:12 +0800 Subject: [PATCH] fix: bug --- app/membership.tsx | 1 + hooks/use-membership.test.ts | 356 +++++++++++++---------------------- jest.setup.js | 6 + 3 files changed, 141 insertions(+), 222 deletions(-) diff --git a/app/membership.tsx b/app/membership.tsx index a24ac88..c5eda7a 100644 --- a/app/membership.tsx +++ b/app/membership.tsx @@ -251,6 +251,7 @@ export default function MembershipScreen() { autoPlayInterval={2000} loop onSnapToItem={(index) => setCurrentImageIndex(index)} + enabled={false} /> {carouselImages.map((_, index) => ( diff --git a/hooks/use-membership.test.ts b/hooks/use-membership.test.ts index be0d04a..47a802f 100644 --- a/hooks/use-membership.test.ts +++ b/hooks/use-membership.test.ts @@ -1,8 +1,6 @@ import { renderHook, act, waitFor } from '@testing-library/react-native' import { useMembership } from './use-membership' import { subscription, useSession } from '@/lib/auth' -import { useRouter } from 'expo-router' -import { Linking, Alert } from 'react-native' jest.mock('@/lib/auth', () => ({ subscription: { @@ -19,47 +17,15 @@ jest.mock('@/lib/auth', () => ({ useSession: jest.fn(), })) -jest.mock('expo-router', () => ({ - useRouter: jest.fn(), -})) - -jest.mock('react-native', () => ({ - Linking: { - openURL: jest.fn(), - }, - Alert: { - alert: jest.fn(), - }, -})) - -const mockUseQuery = jest.fn() -const mockUseMutation = jest.fn() - -jest.mock('@tanstack/react-query', () => ({ - useQuery: (...args: any[]) => mockUseQuery(args[0]), - useMutation: (...args: any[]) => mockUseMutation(args[0]), -})) - describe('useMembership', () => { - const mockRouter = { push: jest.fn() } const mockSession = { user: { id: 'user-123' }, } beforeEach(() => { jest.clearAllMocks() - ;(useRouter as jest.Mock).mockReturnValue(mockRouter) ;(useSession as jest.Mock).mockReturnValue({ data: mockSession }) - - mockUseQuery.mockReturnValue({ - data: null, - isLoading: false, - refetch: jest.fn(), - }) - - mockUseMutation.mockReturnValue({ - mutate: jest.fn(), - }) + ;(subscription.list as jest.Mock).mockResolvedValue({ data: [], error: null }) }) describe('获取 Stripe 定价表数据', () => { @@ -74,16 +40,38 @@ describe('useMembership', () => { }, ], } - - mockUseQuery.mockReturnValue({ + ;(subscription.plans as jest.Mock).mockResolvedValue({ data: mockPricingData, - isLoading: false, - refetch: jest.fn(), + error: null, }) const { result } = renderHook(() => useMembership()) - expect(result.current.stripePricingData).toEqual(mockPricingData) + await waitFor(() => { + expect(result.current.stripePricingData).toEqual(mockPricingData) + }) + + expect(subscription.plans).toHaveBeenCalledWith({ + query: { + id: '', + key: '', + }, + }) + }) + + it('应该处理定价表获取错误', async () => { + ;(subscription.plans as jest.Mock).mockResolvedValue({ + data: null, + error: { message: 'Failed to fetch pricing' }, + }) + + const { result } = renderHook(() => useMembership()) + + await waitFor(() => { + expect(subscription.plans).toHaveBeenCalled() + }) + + expect(result.current.stripePricingData).toBeNull() }) }) @@ -97,77 +85,40 @@ describe('useMembership', () => { stripeSubscriptionId: 'sub-123', }, ] - - let queryCallCount = 0 - mockUseQuery.mockImplementation((config) => { - queryCallCount++ - if (queryCallCount === 2) { - return { - data: mockSubscriptions, - isLoading: false, - refetch: jest.fn(), - } - } - return { - data: null, - isLoading: false, - refetch: jest.fn(), - } + ;(subscription.plans as jest.Mock).mockResolvedValue({ data: null, error: null }) + ;(subscription.list as jest.Mock).mockResolvedValue({ + data: mockSubscriptions, + error: null, }) const { result } = renderHook(() => useMembership()) - expect(result.current.isLoadingSubscriptions).toBe(false) + await waitFor(() => { + expect(subscription.list).toHaveBeenCalledWith({ + query: { referenceId: 'user-123' }, + }) + }) }) it('应该在没有用户会话时不获取订阅', () => { ;(useSession as jest.Mock).mockReturnValue({ data: null }) - - mockUseQuery.mockImplementation((config) => { - if (config.enabled === false) { - return { - data: null, - isLoading: false, - refetch: jest.fn(), - } - } - return { - data: null, - isLoading: false, - refetch: jest.fn(), - } - }) + ;(subscription.plans as jest.Mock).mockResolvedValue({ data: null, error: null }) renderHook(() => useMembership()) - expect(mockUseQuery).toHaveBeenCalled() + expect(subscription.list).not.toHaveBeenCalled() }) }) describe('创建订阅', () => { - it('应该成功创建订阅并打开支付链接', async () => { + it('应该调用 subscription.create', async () => { const mockUrl = 'https://checkout.stripe.com/test' + ;(subscription.plans as jest.Mock).mockResolvedValue({ data: null, error: null }) ;(subscription.create as jest.Mock).mockResolvedValue({ data: { url: mockUrl }, error: null, }) - const mutateFn = jest.fn(async (variables) => { - const { data, error } = await subscription.create({ - priceId: variables.priceId, - productId: variables.productId, - successUrl: expect.stringContaining('/membership'), - cancelUrl: expect.stringContaining('/membership?canceled=true'), - }) - if (error) throw new Error(error.message) - if (data?.url) await Linking.openURL(data.url) - return data - }) - - mockUseMutation.mockReturnValue({ - mutate: mutateFn, - }) - const { result } = renderHook(() => useMembership()) await act(async () => { @@ -177,50 +128,41 @@ describe('useMembership', () => { }) }) - expect(mutateFn).toHaveBeenCalled() - expect(Linking.openURL).toHaveBeenCalledWith(mockUrl) + expect(subscription.create).toHaveBeenCalledWith({ + priceId: 'price-123', + productId: 'prod-123', + successUrl: expect.stringContaining('/membership'), + cancelUrl: expect.stringContaining('/membership?canceled=true'), + }) }) it('应该处理创建订阅错误', async () => { + ;(subscription.plans as jest.Mock).mockResolvedValue({ data: null, error: null }) ;(subscription.create as jest.Mock).mockResolvedValue({ data: null, error: { message: 'Failed to create subscription' }, }) - const mutateFn = jest.fn(async (variables) => { - try { - const { data, error } = await subscription.create(variables) - if (error) throw new Error(error.message) - return data - } catch (error) { - Alert.alert('错误', '订阅创建失败') - throw error - } - }) - - mockUseMutation.mockReturnValue({ - mutate: mutateFn, - }) - const { result } = renderHook(() => useMembership()) await act(async () => { - try { - await result.current.createSubscription({ - priceId: 'price-123', - productId: 'prod-123', - }) - } catch (e) { - // Expected error - } + await result.current.createSubscription({ + priceId: 'price-123', + productId: 'prod-123', + }) }) - expect(Alert.alert).toHaveBeenCalledWith('错误', '订阅创建失败') + expect(subscription.create).toHaveBeenCalled() }) }) describe('升级订阅', () => { it('应该成功升级订阅', async () => { + const mockPricingData = { + pricing_table_items: [ + { amount: '1000', product_name: 'basic', recurring: { interval: 'month' }, metadata: { grant_token: '1000' } }, + ], + } const mockSubscriptions = [ { type: 'licenced', @@ -229,41 +171,37 @@ describe('useMembership', () => { stripeSubscriptionId: 'sub-123', }, ] - - let queryCallCount = 0 - mockUseQuery.mockImplementation(() => { - queryCallCount++ - if (queryCallCount === 2) { - return { - data: mockSubscriptions, - isLoading: false, - refetch: jest.fn(), - } - } - return { - data: null, - isLoading: false, - refetch: jest.fn(), - } - }) - + ;(subscription.plans as jest.Mock).mockResolvedValue({ data: mockPricingData, error: null }) + ;(subscription.list as jest.Mock).mockResolvedValue({ data: mockSubscriptions, error: null }) ;(subscription.upgrade as jest.Mock).mockResolvedValue({ data: { success: true }, error: null, }) - const mutateFn = jest.fn() - mockUseMutation.mockReturnValue({ - mutate: mutateFn, + const { result } = renderHook(() => useMembership()) + + await waitFor(() => { + expect(result.current.activeAuthSubscription).toBeDefined() }) + await act(async () => { + await result.current.upgradeSubscription({ credits: 1000 }) + }) + + expect(subscription.upgrade).toHaveBeenCalled() + }) + + it('应该处理未找到活跃订阅的情况', async () => { + ;(subscription.plans as jest.Mock).mockResolvedValue({ data: null, error: null }) + ;(subscription.list as jest.Mock).mockResolvedValue({ data: [], error: null }) + const { result } = renderHook(() => useMembership()) await act(async () => { - result.current.upgradeSubscription({ credits: 1000 }) + await result.current.upgradeSubscription({ credits: 1000 }) }) - expect(mutateFn).toHaveBeenCalled() + expect(subscription.upgrade).not.toHaveBeenCalled() }) }) @@ -278,127 +216,101 @@ describe('useMembership', () => { referenceId: 'user-123', }, ] - - let queryCallCount = 0 - mockUseQuery.mockImplementation(() => { - queryCallCount++ - if (queryCallCount === 2) { - return { - data: mockSubscriptions, - isLoading: false, - refetch: jest.fn(), - } - } - return { - data: null, - isLoading: false, - refetch: jest.fn(), - } - }) - + ;(subscription.plans as jest.Mock).mockResolvedValue({ data: null, error: null }) + ;(subscription.list as jest.Mock).mockResolvedValue({ data: mockSubscriptions, error: null }) ;(subscription.restore as jest.Mock).mockResolvedValue({ data: { success: true }, error: null, }) - const mutateFn = jest.fn() - mockUseMutation.mockReturnValue({ - mutate: mutateFn, + const { result } = renderHook(() => useMembership()) + + await waitFor(() => { + expect(result.current.activeAuthSubscription).toBeDefined() }) + await act(async () => { + await result.current.restoreSubscription() + }) + + expect(subscription.restore).toHaveBeenCalledWith({ + subscriptionId: 'sub-123', + referenceId: 'user-123', + }) + }) + + it('应该处理未找到可恢复订阅的情况', async () => { + ;(subscription.plans as jest.Mock).mockResolvedValue({ data: null, error: null }) + ;(subscription.list as jest.Mock).mockResolvedValue({ data: [], error: null }) + const { result } = renderHook(() => useMembership()) await act(async () => { - result.current.restoreSubscription() + await result.current.restoreSubscription() }) - expect(mutateFn).toHaveBeenCalled() + expect(subscription.restore).not.toHaveBeenCalled() }) }) describe('充值积分', () => { - it('应该成功充值积分并打开支付链接', async () => { + it('应该成功充值积分', async () => { const mockUrl = 'https://checkout.stripe.com/topup' const mockSubscriptions = [ { type: 'metered', priceId: 'price-metered-123', + creditBalance: { remainingTokenBalance: 100 }, }, ] - - let queryCallCount = 0 - mockUseQuery.mockImplementation(() => { - queryCallCount++ - if (queryCallCount === 2) { - return { - data: mockSubscriptions, - isLoading: false, - refetch: jest.fn(), - } - } - return { - data: null, - isLoading: false, - refetch: jest.fn(), - } - }) - + ;(subscription.plans as jest.Mock).mockResolvedValue({ data: null, error: null }) + ;(subscription.list as jest.Mock).mockResolvedValue({ data: mockSubscriptions, error: null }) ;(subscription.credit.topup as jest.Mock).mockResolvedValue({ data: { url: mockUrl }, error: null, }) - const mutateFn = jest.fn(async (amount) => { - const { data, error } = await subscription.credit.topup({ - amount, - priceId: 'price-metered-123', - successUrl: expect.stringContaining('/membership'), - cancelUrl: expect.stringContaining('/membership?canceled=true'), - }) - if (error) throw new Error(error.message) - if (data?.url) await Linking.openURL(data.url) - return data + const { result } = renderHook(() => useMembership()) + + await waitFor(() => { + expect(result.current.creditBalance).toBe(100) }) - mockUseMutation.mockReturnValue({ - mutate: mutateFn, + await act(async () => { + await result.current.rechargeToken(1000) }) + expect(subscription.credit.topup).toHaveBeenCalledWith({ + amount: 1000, + priceId: 'price-metered-123', + successUrl: expect.stringContaining('/membership'), + cancelUrl: expect.stringContaining('/membership?canceled=true'), + }) + }) + + it('应该处理无效充值金额', async () => { + ;(subscription.plans as jest.Mock).mockResolvedValue({ data: null, error: null }) + + const { result } = renderHook(() => useMembership()) + + await act(async () => { + await result.current.rechargeToken(0) + }) + + expect(subscription.credit.topup).not.toHaveBeenCalled() + }) + + it('应该处理定价数据不可用的情况', async () => { + ;(subscription.plans as jest.Mock).mockResolvedValue({ data: null, error: null }) + ;(subscription.list as jest.Mock).mockResolvedValue({ data: [], error: null }) + const { result } = renderHook(() => useMembership()) await act(async () => { await result.current.rechargeToken(1000) }) - expect(mutateFn).toHaveBeenCalledWith(1000) - expect(Linking.openURL).toHaveBeenCalledWith(mockUrl) - }) - - it('应该处理无效充值金额', async () => { - const mutateFn = jest.fn(async (amount) => { - try { - if (!amount || amount <= 0) throw new Error('充值金额无效') - } catch (error) { - Alert.alert('错误', '充值失败') - throw error - } - }) - - mockUseMutation.mockReturnValue({ - mutate: mutateFn, - }) - - const { result } = renderHook(() => useMembership()) - - await act(async () => { - try { - await result.current.rechargeToken(0) - } catch (e) { - // Expected error - } - }) - - expect(Alert.alert).toHaveBeenCalledWith('错误', '充值失败') + expect(subscription.credit.topup).not.toHaveBeenCalled() }) }) }) diff --git a/jest.setup.js b/jest.setup.js index 4f8ef0e..84cab50 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -43,6 +43,12 @@ jest.mock('react-native', () => { get: jest.fn(), set: jest.fn(), }, + Alert: { + alert: jest.fn(), + }, + Linking: { + openURL: jest.fn(), + }, } })