mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-26 17:54:14 +00:00
## Summary Backport of #6064 (subscription page) to the `rh-test` branch. This PR manually cherry-picks commit7e1e8e3b65to the rh-test branch and resolves merge conflicts that prevented automatic backporting. ## Conflicts Resolved ### 1. `src/components/actionbar/ComfyActionbar.vue` - **Conflict**: HEAD (rh-test) used `<ComfyQueueButton />` while the subscription PR introduced `<ComfyRunButton />` - **Resolution**: Updated to use `<ComfyRunButton />` to include the subscription functionality wrapper while maintaining the existing rh-test template structure ### 2. `src/composables/auth/useFirebaseAuthActions.ts` - **Conflict**: Simple ordering difference in the return statement - **Resolution**: Used the subscription PR's ordering: `deleteAccount, accessError, reportError` ## Testing The cherry-pick completed successfully and passed all pre-commit hooks: - ✅ ESLint - ✅ Prettier formatting - ⚠️ Note: 2 unused i18n keys detected (informational only, same as original PR) ## Related - Original PR: #6064 - Cherry-picked commit:7e1e8e3b65┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6140-backport-subscription-page-to-rh-test-2916d73d365081f38f00df422004f61a) by [Unito](https://www.unito.io) Co-authored-by: Terry Jia <terryjia88@gmail.com> Co-authored-by: GitHub Action <action@github.com>
322 lines
8.8 KiB
TypeScript
322 lines
8.8 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { ref } from 'vue'
|
|
|
|
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
|
|
|
// Create mocks
|
|
const mockIsLoggedIn = ref(false)
|
|
const mockReportError = vi.fn()
|
|
const mockAccessBillingPortal = vi.fn()
|
|
const mockShowSubscriptionRequiredDialog = vi.fn()
|
|
const mockGetAuthHeader = vi.fn(() =>
|
|
Promise.resolve({ Authorization: 'Bearer test-token' })
|
|
)
|
|
|
|
// Mock dependencies
|
|
vi.mock('@/composables/auth/useCurrentUser', () => ({
|
|
useCurrentUser: vi.fn(() => ({
|
|
isLoggedIn: mockIsLoggedIn
|
|
}))
|
|
}))
|
|
|
|
vi.mock('@/composables/auth/useFirebaseAuthActions', () => ({
|
|
useFirebaseAuthActions: vi.fn(() => ({
|
|
reportError: mockReportError,
|
|
accessBillingPortal: mockAccessBillingPortal
|
|
}))
|
|
}))
|
|
|
|
vi.mock('@/composables/useErrorHandling', () => ({
|
|
useErrorHandling: vi.fn(() => ({
|
|
wrapWithErrorHandlingAsync: vi.fn(
|
|
(fn, errorHandler) =>
|
|
async (...args: any[]) => {
|
|
try {
|
|
return await fn(...args)
|
|
} catch (error) {
|
|
if (errorHandler) {
|
|
errorHandler(error)
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
)
|
|
}))
|
|
}))
|
|
|
|
vi.mock('@/platform/distribution/types', () => ({
|
|
isCloud: true
|
|
}))
|
|
|
|
vi.mock('@/services/dialogService', () => ({
|
|
useDialogService: vi.fn(() => ({
|
|
showSubscriptionRequiredDialog: mockShowSubscriptionRequiredDialog
|
|
}))
|
|
}))
|
|
|
|
vi.mock('@/stores/firebaseAuthStore', () => ({
|
|
useFirebaseAuthStore: vi.fn(() => ({
|
|
getAuthHeader: mockGetAuthHeader
|
|
})),
|
|
FirebaseAuthStoreError: class extends Error {}
|
|
}))
|
|
|
|
// Mock fetch
|
|
global.fetch = vi.fn()
|
|
|
|
describe('useSubscription', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockIsLoggedIn.value = false
|
|
vi.mocked(global.fetch).mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
is_active: false,
|
|
subscription_id: '',
|
|
renewal_date: ''
|
|
})
|
|
} as Response)
|
|
})
|
|
|
|
describe('computed properties', () => {
|
|
it('should compute isActiveSubscription correctly when subscription is active', async () => {
|
|
vi.mocked(global.fetch).mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
is_active: true,
|
|
subscription_id: 'sub_123',
|
|
renewal_date: '2025-11-16'
|
|
})
|
|
} as Response)
|
|
|
|
mockIsLoggedIn.value = true
|
|
const { isActiveSubscription, fetchStatus } = useSubscription()
|
|
|
|
await fetchStatus()
|
|
expect(isActiveSubscription.value).toBe(true)
|
|
})
|
|
|
|
it('should compute isActiveSubscription as false when subscription is inactive', async () => {
|
|
vi.mocked(global.fetch).mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
is_active: false,
|
|
subscription_id: 'sub_123',
|
|
renewal_date: '2025-11-16'
|
|
})
|
|
} as Response)
|
|
|
|
mockIsLoggedIn.value = true
|
|
const { isActiveSubscription, fetchStatus } = useSubscription()
|
|
|
|
await fetchStatus()
|
|
expect(isActiveSubscription.value).toBe(false)
|
|
})
|
|
|
|
it('should format renewal date correctly', async () => {
|
|
vi.mocked(global.fetch).mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
is_active: true,
|
|
subscription_id: 'sub_123',
|
|
renewal_date: '2025-11-16T12:00:00Z'
|
|
})
|
|
} as Response)
|
|
|
|
mockIsLoggedIn.value = true
|
|
const { formattedRenewalDate, fetchStatus } = useSubscription()
|
|
|
|
await fetchStatus()
|
|
// The date format may vary based on timezone, so we just check it's a valid date string
|
|
expect(formattedRenewalDate.value).toMatch(/^[A-Za-z]{3} \d{1,2}, \d{4}$/)
|
|
expect(formattedRenewalDate.value).toContain('2025')
|
|
expect(formattedRenewalDate.value).toContain('Nov')
|
|
})
|
|
|
|
it('should return empty string when renewal date is not available', () => {
|
|
const { formattedRenewalDate } = useSubscription()
|
|
|
|
expect(formattedRenewalDate.value).toBe('')
|
|
})
|
|
|
|
it('should format monthly price correctly', () => {
|
|
const { formattedMonthlyPrice } = useSubscription()
|
|
|
|
expect(formattedMonthlyPrice.value).toBe('$20')
|
|
})
|
|
})
|
|
|
|
describe('fetchStatus', () => {
|
|
it('should fetch subscription status successfully', async () => {
|
|
const mockStatus = {
|
|
is_active: true,
|
|
subscription_id: 'sub_123',
|
|
renewal_date: '2025-11-16'
|
|
}
|
|
|
|
vi.mocked(global.fetch).mockResolvedValue({
|
|
ok: true,
|
|
json: async () => mockStatus
|
|
} as Response)
|
|
|
|
mockIsLoggedIn.value = true
|
|
const { fetchStatus } = useSubscription()
|
|
|
|
await fetchStatus()
|
|
|
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
expect.stringContaining('/customers/cloud-subscription-status'),
|
|
expect.objectContaining({
|
|
headers: expect.objectContaining({
|
|
Authorization: 'Bearer test-token',
|
|
'Content-Type': 'application/json'
|
|
})
|
|
})
|
|
)
|
|
})
|
|
|
|
it('should handle fetch errors gracefully', async () => {
|
|
vi.mocked(global.fetch).mockResolvedValue({
|
|
ok: false,
|
|
json: async () => ({ message: 'Subscription not found' })
|
|
} as Response)
|
|
|
|
const { fetchStatus } = useSubscription()
|
|
|
|
await expect(fetchStatus()).rejects.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('subscribe', () => {
|
|
it('should initiate subscription checkout successfully', async () => {
|
|
const checkoutUrl = 'https://checkout.stripe.com/test'
|
|
|
|
vi.mocked(global.fetch).mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({ checkout_url: checkoutUrl })
|
|
} as Response)
|
|
|
|
// Mock window.open
|
|
const windowOpenSpy = vi
|
|
.spyOn(window, 'open')
|
|
.mockImplementation(() => null)
|
|
|
|
const { subscribe } = useSubscription()
|
|
|
|
await subscribe()
|
|
|
|
expect(global.fetch).toHaveBeenCalledWith(
|
|
expect.stringContaining('/customers/cloud-subscription-checkout'),
|
|
expect.objectContaining({
|
|
method: 'POST',
|
|
headers: expect.objectContaining({
|
|
Authorization: 'Bearer test-token',
|
|
'Content-Type': 'application/json'
|
|
})
|
|
})
|
|
)
|
|
|
|
expect(windowOpenSpy).toHaveBeenCalledWith(checkoutUrl, '_blank')
|
|
|
|
windowOpenSpy.mockRestore()
|
|
})
|
|
|
|
it('should throw error when checkout URL is not returned', async () => {
|
|
vi.mocked(global.fetch).mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({})
|
|
} as Response)
|
|
|
|
const { subscribe } = useSubscription()
|
|
|
|
await expect(subscribe()).rejects.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('requireActiveSubscription', () => {
|
|
it('should not show dialog when subscription is active', async () => {
|
|
vi.mocked(global.fetch).mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
is_active: true,
|
|
subscription_id: 'sub_123',
|
|
renewal_date: '2025-11-16'
|
|
})
|
|
} as Response)
|
|
|
|
const { requireActiveSubscription } = useSubscription()
|
|
|
|
await requireActiveSubscription()
|
|
|
|
expect(mockShowSubscriptionRequiredDialog).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should show dialog when subscription is inactive', async () => {
|
|
vi.mocked(global.fetch).mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({
|
|
is_active: false,
|
|
subscription_id: 'sub_123',
|
|
renewal_date: '2025-11-16'
|
|
})
|
|
} as Response)
|
|
|
|
const { requireActiveSubscription } = useSubscription()
|
|
|
|
await requireActiveSubscription()
|
|
|
|
expect(mockShowSubscriptionRequiredDialog).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('action handlers', () => {
|
|
it('should open usage history URL', () => {
|
|
const windowOpenSpy = vi
|
|
.spyOn(window, 'open')
|
|
.mockImplementation(() => null)
|
|
|
|
const { handleViewUsageHistory } = useSubscription()
|
|
handleViewUsageHistory()
|
|
|
|
expect(windowOpenSpy).toHaveBeenCalledWith(
|
|
'https://platform.comfy.org/profile/usage',
|
|
'_blank'
|
|
)
|
|
|
|
windowOpenSpy.mockRestore()
|
|
})
|
|
|
|
it('should open learn more URL', () => {
|
|
const windowOpenSpy = vi
|
|
.spyOn(window, 'open')
|
|
.mockImplementation(() => null)
|
|
|
|
const { handleLearnMore } = useSubscription()
|
|
handleLearnMore()
|
|
|
|
expect(windowOpenSpy).toHaveBeenCalledWith(
|
|
'https://docs.comfy.org',
|
|
'_blank'
|
|
)
|
|
|
|
windowOpenSpy.mockRestore()
|
|
})
|
|
|
|
it('should call accessBillingPortal for invoice history', async () => {
|
|
const { handleInvoiceHistory } = useSubscription()
|
|
|
|
await handleInvoiceHistory()
|
|
|
|
expect(mockAccessBillingPortal).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should call accessBillingPortal for manage subscription', async () => {
|
|
const { manageSubscription } = useSubscription()
|
|
|
|
await manageSubscription()
|
|
|
|
expect(mockAccessBillingPortal).toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|