mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-17 21:21:06 +00:00
Compare commits
1 Commits
test/cover
...
fix/11077-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
944e0e8118 |
@@ -1,165 +0,0 @@
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import CloudSubscriptionRedirectView from './CloudSubscriptionRedirectView.vue'
|
||||
|
||||
const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
// Router mocks
|
||||
let mockQuery: Record<string, unknown> = {}
|
||||
const mockRouterPush = vi.fn()
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRoute: () => ({
|
||||
query: mockQuery
|
||||
}),
|
||||
useRouter: () => ({
|
||||
push: mockRouterPush
|
||||
})
|
||||
}))
|
||||
|
||||
// Firebase / subscription mocks
|
||||
const authActionMocks = vi.hoisted(() => ({
|
||||
reportError: vi.fn(),
|
||||
accessBillingPortal: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/auth/useAuthActions', () => ({
|
||||
useAuthActions: () => authActionMocks
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useErrorHandling', () => ({
|
||||
useErrorHandling: () => ({
|
||||
wrapWithErrorHandlingAsync:
|
||||
<T extends (...args: never[]) => unknown>(fn: T) =>
|
||||
(...args: Parameters<T>) =>
|
||||
fn(...args)
|
||||
})
|
||||
}))
|
||||
|
||||
const subscriptionMocks = vi.hoisted(() => ({
|
||||
isActiveSubscription: { value: false },
|
||||
isInitialized: { value: true },
|
||||
subscriptionStatus: { value: null }
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({
|
||||
useSubscription: () => subscriptionMocks
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
useBillingContext: () => subscriptionMocks
|
||||
}))
|
||||
|
||||
// Avoid real network / isCloud behavior
|
||||
const mockPerformSubscriptionCheckout = vi.fn()
|
||||
vi.mock('@/platform/cloud/subscription/utils/subscriptionCheckoutUtil', () => ({
|
||||
performSubscriptionCheckout: (...args: unknown[]) =>
|
||||
mockPerformSubscriptionCheckout(...args)
|
||||
}))
|
||||
|
||||
const createI18nInstance = () =>
|
||||
createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: {
|
||||
cloudOnboarding: {
|
||||
skipToCloudApp: 'Skip to the cloud app'
|
||||
},
|
||||
g: {
|
||||
comfyOrgLogoAlt: 'Comfy org logo'
|
||||
},
|
||||
subscription: {
|
||||
subscribeTo: 'Subscribe to {plan}',
|
||||
tiers: {
|
||||
standard: { name: 'Standard' },
|
||||
creator: { name: 'Creator' },
|
||||
pro: { name: 'Pro' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const mountView = async (query: Record<string, unknown>) => {
|
||||
mockQuery = query
|
||||
|
||||
const { container } = render(CloudSubscriptionRedirectView, {
|
||||
global: {
|
||||
plugins: [createI18nInstance()]
|
||||
}
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
|
||||
return { container }
|
||||
}
|
||||
|
||||
describe('CloudSubscriptionRedirectView', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockQuery = {}
|
||||
subscriptionMocks.isActiveSubscription.value = false
|
||||
subscriptionMocks.isInitialized.value = true
|
||||
})
|
||||
|
||||
test('redirects to home when subscriptionType is missing', async () => {
|
||||
await mountView({})
|
||||
|
||||
expect(mockRouterPush).toHaveBeenCalledWith('/')
|
||||
})
|
||||
|
||||
test('redirects to home when subscriptionType is invalid', async () => {
|
||||
await mountView({ tier: 'invalid' })
|
||||
|
||||
expect(mockRouterPush).toHaveBeenCalledWith('/')
|
||||
})
|
||||
|
||||
test('shows subscription copy when subscriptionType is valid', async () => {
|
||||
await mountView({ tier: 'creator' })
|
||||
|
||||
// Should not redirect to home
|
||||
expect(mockRouterPush).not.toHaveBeenCalledWith('/')
|
||||
|
||||
// Shows copy under logo
|
||||
expect(screen.getByText('Subscribe to Creator')).toBeInTheDocument()
|
||||
|
||||
// Triggers checkout flow
|
||||
expect(mockPerformSubscriptionCheckout).toHaveBeenCalledWith(
|
||||
'creator',
|
||||
'monthly',
|
||||
false
|
||||
)
|
||||
|
||||
// Shows loading affordances
|
||||
expect(
|
||||
screen.getByRole('link', { name: /skip to the cloud app/i })
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('opens billing portal when subscription is already active', async () => {
|
||||
subscriptionMocks.isActiveSubscription.value = true
|
||||
|
||||
await mountView({ tier: 'creator' })
|
||||
|
||||
expect(mockRouterPush).not.toHaveBeenCalledWith('/')
|
||||
expect(authActionMocks.accessBillingPortal).toHaveBeenCalledTimes(1)
|
||||
expect(mockPerformSubscriptionCheckout).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('uses first value when subscriptionType is an array', async () => {
|
||||
await mountView({
|
||||
tier: ['creator', 'pro']
|
||||
})
|
||||
|
||||
expect(mockRouterPush).not.toHaveBeenCalledWith('/')
|
||||
expect(screen.getByText('Subscribe to Creator')).toBeInTheDocument()
|
||||
expect(mockPerformSubscriptionCheckout).toHaveBeenCalledWith(
|
||||
'creator',
|
||||
'monthly',
|
||||
false
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,127 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { useBillingContext } from '@/composables/billing/useBillingContext'
|
||||
import { useAuthActions } from '@/composables/auth/useAuthActions'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import type { TierKey } from '@/platform/cloud/subscription/constants/tierPricing'
|
||||
import { performSubscriptionCheckout } from '@/platform/cloud/subscription/utils/subscriptionCheckoutUtil'
|
||||
|
||||
import type { BillingCycle } from '../subscription/utils/subscriptionTierRank'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { reportError, accessBillingPortal } = useAuthActions()
|
||||
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||
|
||||
const { isActiveSubscription, isInitialized, initialize } = useBillingContext()
|
||||
|
||||
const selectedTierKey = ref<TierKey | null>(null)
|
||||
|
||||
const tierDisplayName = computed(() => {
|
||||
if (!selectedTierKey.value) return ''
|
||||
const names: Record<TierKey, string> = {
|
||||
free: t('subscription.tiers.free.name'),
|
||||
standard: t('subscription.tiers.standard.name'),
|
||||
creator: t('subscription.tiers.creator.name'),
|
||||
pro: t('subscription.tiers.pro.name'),
|
||||
founder: t('subscription.tiers.founder.name')
|
||||
}
|
||||
return names[selectedTierKey.value]
|
||||
})
|
||||
|
||||
const runRedirect = wrapWithErrorHandlingAsync(async () => {
|
||||
const rawType = route.query.tier
|
||||
const rawCycle = route.query.cycle
|
||||
let tierKeyParam: string | null = null
|
||||
let cycleParam = 'monthly'
|
||||
|
||||
if (typeof rawType === 'string') {
|
||||
tierKeyParam = rawType
|
||||
} else if (Array.isArray(rawType) && rawType[0]) {
|
||||
tierKeyParam = rawType[0]
|
||||
}
|
||||
|
||||
if (typeof rawCycle === 'string') {
|
||||
cycleParam = rawCycle
|
||||
} else if (Array.isArray(rawCycle) && rawCycle[0]) {
|
||||
cycleParam = rawCycle[0]
|
||||
}
|
||||
|
||||
if (!tierKeyParam) {
|
||||
await router.push('/')
|
||||
return
|
||||
}
|
||||
|
||||
// Only paid tiers can be checked out via redirect
|
||||
const validTierKeys: TierKey[] = ['standard', 'creator', 'pro', 'founder']
|
||||
if (!(validTierKeys as string[]).includes(tierKeyParam)) {
|
||||
await router.push('/')
|
||||
return
|
||||
}
|
||||
|
||||
const tierKey = tierKeyParam as TierKey
|
||||
|
||||
selectedTierKey.value = tierKey
|
||||
|
||||
const validCycles: BillingCycle[] = ['monthly', 'yearly']
|
||||
if (!cycleParam || !(validCycles as string[]).includes(cycleParam)) {
|
||||
cycleParam = 'monthly'
|
||||
}
|
||||
|
||||
if (!isInitialized.value) {
|
||||
await initialize()
|
||||
}
|
||||
|
||||
if (isActiveSubscription.value) {
|
||||
await accessBillingPortal(undefined, false)
|
||||
} else {
|
||||
await performSubscriptionCheckout(
|
||||
tierKey,
|
||||
cycleParam as BillingCycle,
|
||||
false
|
||||
)
|
||||
}
|
||||
}, reportError)
|
||||
|
||||
onMounted(() => {
|
||||
void runRedirect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="bg-comfy-menu-secondary-bg flex size-full items-center justify-center"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<img
|
||||
src="/assets/images/comfy-logo-single.svg"
|
||||
:alt="t('g.comfyOrgLogoAlt')"
|
||||
class="size-16"
|
||||
/>
|
||||
<p
|
||||
v-if="selectedTierKey"
|
||||
class="font-inter text-base/normal font-normal text-base-foreground"
|
||||
>
|
||||
{{
|
||||
t('subscription.subscribeTo', {
|
||||
plan: tierDisplayName
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
<ProgressSpinner v-if="selectedTierKey" class="size-8" stroke-width="4" />
|
||||
<Button
|
||||
v-if="selectedTierKey"
|
||||
as="a"
|
||||
href="/"
|
||||
link
|
||||
:label="t('cloudOnboarding.skipToCloudApp')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -77,13 +77,6 @@ export const cloudOnboardingRoutes: RouteRecordRaw[] = [
|
||||
component: () =>
|
||||
import('@/platform/cloud/onboarding/CloudAuthTimeoutView.vue'),
|
||||
props: true
|
||||
},
|
||||
{
|
||||
path: 'subscribe',
|
||||
name: 'cloud-subscribe',
|
||||
component: () =>
|
||||
import('@/platform/cloud/onboarding/CloudSubscriptionRedirectView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user