This commit is contained in:
imeepos
2026-01-28 14:46:12 +08:00
parent ca63868282
commit ea53d8d70e
3 changed files with 141 additions and 222 deletions

View File

@@ -251,6 +251,7 @@ export default function MembershipScreen() {
autoPlayInterval={2000} autoPlayInterval={2000}
loop loop
onSnapToItem={(index) => setCurrentImageIndex(index)} onSnapToItem={(index) => setCurrentImageIndex(index)}
enabled={false}
/> />
<View style={styles.dotsContainer}> <View style={styles.dotsContainer}>
{carouselImages.map((_, index) => ( {carouselImages.map((_, index) => (

View File

@@ -1,8 +1,6 @@
import { renderHook, act, waitFor } from '@testing-library/react-native' import { renderHook, act, waitFor } from '@testing-library/react-native'
import { useMembership } from './use-membership' import { useMembership } from './use-membership'
import { subscription, useSession } from '@/lib/auth' import { subscription, useSession } from '@/lib/auth'
import { useRouter } from 'expo-router'
import { Linking, Alert } from 'react-native'
jest.mock('@/lib/auth', () => ({ jest.mock('@/lib/auth', () => ({
subscription: { subscription: {
@@ -19,47 +17,15 @@ jest.mock('@/lib/auth', () => ({
useSession: jest.fn(), 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', () => { describe('useMembership', () => {
const mockRouter = { push: jest.fn() }
const mockSession = { const mockSession = {
user: { id: 'user-123' }, user: { id: 'user-123' },
} }
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks() jest.clearAllMocks()
;(useRouter as jest.Mock).mockReturnValue(mockRouter)
;(useSession as jest.Mock).mockReturnValue({ data: mockSession }) ;(useSession as jest.Mock).mockReturnValue({ data: mockSession })
;(subscription.list as jest.Mock).mockResolvedValue({ data: [], error: null })
mockUseQuery.mockReturnValue({
data: null,
isLoading: false,
refetch: jest.fn(),
})
mockUseMutation.mockReturnValue({
mutate: jest.fn(),
})
}) })
describe('获取 Stripe 定价表数据', () => { describe('获取 Stripe 定价表数据', () => {
@@ -74,16 +40,38 @@ describe('useMembership', () => {
}, },
], ],
} }
;(subscription.plans as jest.Mock).mockResolvedValue({
mockUseQuery.mockReturnValue({
data: mockPricingData, data: mockPricingData,
isLoading: false, error: null,
refetch: jest.fn(),
}) })
const { result } = renderHook(() => useMembership()) 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', stripeSubscriptionId: 'sub-123',
}, },
] ]
;(subscription.plans as jest.Mock).mockResolvedValue({ data: null, error: null })
let queryCallCount = 0 ;(subscription.list as jest.Mock).mockResolvedValue({
mockUseQuery.mockImplementation((config) => { data: mockSubscriptions,
queryCallCount++ error: null,
if (queryCallCount === 2) {
return {
data: mockSubscriptions,
isLoading: false,
refetch: jest.fn(),
}
}
return {
data: null,
isLoading: false,
refetch: jest.fn(),
}
}) })
const { result } = renderHook(() => useMembership()) const { result } = renderHook(() => useMembership())
expect(result.current.isLoadingSubscriptions).toBe(false) await waitFor(() => {
expect(subscription.list).toHaveBeenCalledWith({
query: { referenceId: 'user-123' },
})
})
}) })
it('应该在没有用户会话时不获取订阅', () => { it('应该在没有用户会话时不获取订阅', () => {
;(useSession as jest.Mock).mockReturnValue({ data: null }) ;(useSession as jest.Mock).mockReturnValue({ data: null })
;(subscription.plans as jest.Mock).mockResolvedValue({ data: null, error: null })
mockUseQuery.mockImplementation((config) => {
if (config.enabled === false) {
return {
data: null,
isLoading: false,
refetch: jest.fn(),
}
}
return {
data: null,
isLoading: false,
refetch: jest.fn(),
}
})
renderHook(() => useMembership()) renderHook(() => useMembership())
expect(mockUseQuery).toHaveBeenCalled() expect(subscription.list).not.toHaveBeenCalled()
}) })
}) })
describe('创建订阅', () => { describe('创建订阅', () => {
it('应该成功创建订阅并打开支付链接', async () => { it('应该调用 subscription.create', async () => {
const mockUrl = 'https://checkout.stripe.com/test' const mockUrl = 'https://checkout.stripe.com/test'
;(subscription.plans as jest.Mock).mockResolvedValue({ data: null, error: null })
;(subscription.create as jest.Mock).mockResolvedValue({ ;(subscription.create as jest.Mock).mockResolvedValue({
data: { url: mockUrl }, data: { url: mockUrl },
error: null, 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()) const { result } = renderHook(() => useMembership())
await act(async () => { await act(async () => {
@@ -177,50 +128,41 @@ describe('useMembership', () => {
}) })
}) })
expect(mutateFn).toHaveBeenCalled() expect(subscription.create).toHaveBeenCalledWith({
expect(Linking.openURL).toHaveBeenCalledWith(mockUrl) priceId: 'price-123',
productId: 'prod-123',
successUrl: expect.stringContaining('/membership'),
cancelUrl: expect.stringContaining('/membership?canceled=true'),
})
}) })
it('应该处理创建订阅错误', async () => { it('应该处理创建订阅错误', async () => {
;(subscription.plans as jest.Mock).mockResolvedValue({ data: null, error: null })
;(subscription.create as jest.Mock).mockResolvedValue({ ;(subscription.create as jest.Mock).mockResolvedValue({
data: null, data: null,
error: { message: 'Failed to create subscription' }, 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()) const { result } = renderHook(() => useMembership())
await act(async () => { await act(async () => {
try { await result.current.createSubscription({
await result.current.createSubscription({ priceId: 'price-123',
priceId: 'price-123', productId: 'prod-123',
productId: 'prod-123', })
})
} catch (e) {
// Expected error
}
}) })
expect(Alert.alert).toHaveBeenCalledWith('错误', '订阅创建失败') expect(subscription.create).toHaveBeenCalled()
}) })
}) })
describe('升级订阅', () => { describe('升级订阅', () => {
it('应该成功升级订阅', async () => { it('应该成功升级订阅', async () => {
const mockPricingData = {
pricing_table_items: [
{ amount: '1000', product_name: 'basic', recurring: { interval: 'month' }, metadata: { grant_token: '1000' } },
],
}
const mockSubscriptions = [ const mockSubscriptions = [
{ {
type: 'licenced', type: 'licenced',
@@ -229,41 +171,37 @@ describe('useMembership', () => {
stripeSubscriptionId: 'sub-123', stripeSubscriptionId: 'sub-123',
}, },
] ]
;(subscription.plans as jest.Mock).mockResolvedValue({ data: mockPricingData, error: null })
let queryCallCount = 0 ;(subscription.list as jest.Mock).mockResolvedValue({ data: mockSubscriptions, error: null })
mockUseQuery.mockImplementation(() => {
queryCallCount++
if (queryCallCount === 2) {
return {
data: mockSubscriptions,
isLoading: false,
refetch: jest.fn(),
}
}
return {
data: null,
isLoading: false,
refetch: jest.fn(),
}
})
;(subscription.upgrade as jest.Mock).mockResolvedValue({ ;(subscription.upgrade as jest.Mock).mockResolvedValue({
data: { success: true }, data: { success: true },
error: null, error: null,
}) })
const mutateFn = jest.fn() const { result } = renderHook(() => useMembership())
mockUseMutation.mockReturnValue({
mutate: mutateFn, 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()) const { result } = renderHook(() => useMembership())
await act(async () => { 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', referenceId: 'user-123',
}, },
] ]
;(subscription.plans as jest.Mock).mockResolvedValue({ data: null, error: null })
let queryCallCount = 0 ;(subscription.list as jest.Mock).mockResolvedValue({ data: mockSubscriptions, error: null })
mockUseQuery.mockImplementation(() => {
queryCallCount++
if (queryCallCount === 2) {
return {
data: mockSubscriptions,
isLoading: false,
refetch: jest.fn(),
}
}
return {
data: null,
isLoading: false,
refetch: jest.fn(),
}
})
;(subscription.restore as jest.Mock).mockResolvedValue({ ;(subscription.restore as jest.Mock).mockResolvedValue({
data: { success: true }, data: { success: true },
error: null, error: null,
}) })
const mutateFn = jest.fn() const { result } = renderHook(() => useMembership())
mockUseMutation.mockReturnValue({
mutate: mutateFn, 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()) const { result } = renderHook(() => useMembership())
await act(async () => { await act(async () => {
result.current.restoreSubscription() await result.current.restoreSubscription()
}) })
expect(mutateFn).toHaveBeenCalled() expect(subscription.restore).not.toHaveBeenCalled()
}) })
}) })
describe('充值积分', () => { describe('充值积分', () => {
it('应该成功充值积分并打开支付链接', async () => { it('应该成功充值积分', async () => {
const mockUrl = 'https://checkout.stripe.com/topup' const mockUrl = 'https://checkout.stripe.com/topup'
const mockSubscriptions = [ const mockSubscriptions = [
{ {
type: 'metered', type: 'metered',
priceId: 'price-metered-123', priceId: 'price-metered-123',
creditBalance: { remainingTokenBalance: 100 },
}, },
] ]
;(subscription.plans as jest.Mock).mockResolvedValue({ data: null, error: null })
let queryCallCount = 0 ;(subscription.list as jest.Mock).mockResolvedValue({ data: mockSubscriptions, error: null })
mockUseQuery.mockImplementation(() => {
queryCallCount++
if (queryCallCount === 2) {
return {
data: mockSubscriptions,
isLoading: false,
refetch: jest.fn(),
}
}
return {
data: null,
isLoading: false,
refetch: jest.fn(),
}
})
;(subscription.credit.topup as jest.Mock).mockResolvedValue({ ;(subscription.credit.topup as jest.Mock).mockResolvedValue({
data: { url: mockUrl }, data: { url: mockUrl },
error: null, error: null,
}) })
const mutateFn = jest.fn(async (amount) => { const { result } = renderHook(() => useMembership())
const { data, error } = await subscription.credit.topup({
amount, await waitFor(() => {
priceId: 'price-metered-123', expect(result.current.creditBalance).toBe(100)
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({ await act(async () => {
mutate: mutateFn, 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()) const { result } = renderHook(() => useMembership())
await act(async () => { await act(async () => {
await result.current.rechargeToken(1000) await result.current.rechargeToken(1000)
}) })
expect(mutateFn).toHaveBeenCalledWith(1000) expect(subscription.credit.topup).not.toHaveBeenCalled()
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('错误', '充值失败')
}) })
}) })
}) })

View File

@@ -43,6 +43,12 @@ jest.mock('react-native', () => {
get: jest.fn(), get: jest.fn(),
set: jest.fn(), set: jest.fn(),
}, },
Alert: {
alert: jest.fn(),
},
Linking: {
openURL: jest.fn(),
},
} }
}) })