mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-12 10:17:08 +00:00
Compare commits
2 Commits
fix/websit
...
glary/rena
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dfb024df0 | ||
|
|
d93666d160 |
@@ -4,11 +4,11 @@ import { nextTick, ref } from 'vue'
|
||||
|
||||
import CloudRunButtonWrapper from './CloudRunButtonWrapper.vue'
|
||||
|
||||
const mockIsActiveSubscription = ref(true)
|
||||
const mockCanAccessSubscriptionFeatures = ref(true)
|
||||
|
||||
vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
useBillingContext: () => ({
|
||||
isActiveSubscription: mockIsActiveSubscription
|
||||
canAccessSubscriptionFeatures: mockCanAccessSubscriptionFeatures
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -32,7 +32,7 @@ function renderWrapper() {
|
||||
|
||||
describe('CloudRunButtonWrapper', () => {
|
||||
beforeEach(() => {
|
||||
mockIsActiveSubscription.value = true
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
})
|
||||
|
||||
it('renders the runnable queue button when the subscription is active', () => {
|
||||
@@ -45,7 +45,7 @@ describe('CloudRunButtonWrapper', () => {
|
||||
})
|
||||
|
||||
it('locks the run button when the subscription is inactive', () => {
|
||||
mockIsActiveSubscription.value = false
|
||||
mockCanAccessSubscriptionFeatures.value = false
|
||||
renderWrapper()
|
||||
|
||||
expect(screen.getByTestId('subscribe-to-run-button')).toBeInTheDocument()
|
||||
@@ -53,12 +53,12 @@ describe('CloudRunButtonWrapper', () => {
|
||||
})
|
||||
|
||||
it('unlocks the run button once the subscription becomes active again', async () => {
|
||||
mockIsActiveSubscription.value = false
|
||||
mockCanAccessSubscriptionFeatures.value = false
|
||||
renderWrapper()
|
||||
|
||||
expect(screen.getByTestId('subscribe-to-run-button')).toBeInTheDocument()
|
||||
|
||||
mockIsActiveSubscription.value = true
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
await nextTick()
|
||||
|
||||
expect(screen.getByTestId('queue-button')).toBeInTheDocument()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<component
|
||||
:is="currentButton"
|
||||
:key="isActiveSubscription ? 'queue' : 'subscribe'"
|
||||
:key="canAccessSubscriptionFeatures ? 'queue' : 'subscribe'"
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
@@ -11,9 +11,9 @@ import ComfyQueueButton from '@/components/actionbar/ComfyRunButton/ComfyQueueBu
|
||||
import { useBillingContext } from '@/composables/billing/useBillingContext'
|
||||
import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue'
|
||||
|
||||
const { isActiveSubscription } = useBillingContext()
|
||||
const { canAccessSubscriptionFeatures } = useBillingContext()
|
||||
|
||||
const currentButton = computed(() =>
|
||||
isActiveSubscription.value ? ComfyQueueButton : SubscribeToRunButton
|
||||
canAccessSubscriptionFeatures.value ? ComfyQueueButton : SubscribeToRunButton
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -64,7 +64,7 @@ function makeSubscription(
|
||||
|
||||
const mockFetchStatus = vi.fn().mockResolvedValue(undefined)
|
||||
const mockFetchBalance = vi.fn().mockResolvedValue(undefined)
|
||||
const mockIsActiveSubscription = ref(true)
|
||||
const mockCanAccessSubscriptionFeatures = ref(true)
|
||||
const mockIsFreeTier = ref(false)
|
||||
const mockTier = ref<SubscriptionInfo['tier']>('CREATOR')
|
||||
const mockSubscription = ref<SubscriptionInfo | null>(makeSubscription())
|
||||
@@ -73,7 +73,7 @@ const mockIsLoading = ref(false)
|
||||
|
||||
vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
useBillingContext: vi.fn(() => ({
|
||||
isActiveSubscription: mockIsActiveSubscription,
|
||||
canAccessSubscriptionFeatures: mockCanAccessSubscriptionFeatures,
|
||||
isFreeTier: mockIsFreeTier,
|
||||
tier: mockTier,
|
||||
subscription: mockSubscription,
|
||||
@@ -153,7 +153,7 @@ describe('CurrentUserPopoverLegacy', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsCloud.value = true
|
||||
mockIsActiveSubscription.value = true
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
mockIsFreeTier.value = false
|
||||
mockTier.value = 'CREATOR'
|
||||
mockSubscription.value = makeSubscription()
|
||||
@@ -203,7 +203,7 @@ describe('CurrentUserPopoverLegacy', () => {
|
||||
})
|
||||
|
||||
it('refreshes subscription status through the billing facade after subscribing', async () => {
|
||||
mockIsActiveSubscription.value = false
|
||||
mockCanAccessSubscriptionFeatures.value = false
|
||||
const { user } = renderComponent()
|
||||
|
||||
await user.click(screen.getByTestId('subscribe-button-mock'))
|
||||
@@ -478,7 +478,7 @@ describe('CurrentUserPopoverLegacy', () => {
|
||||
})
|
||||
|
||||
it('hides subscribe button', () => {
|
||||
mockIsActiveSubscription.value = false
|
||||
mockCanAccessSubscriptionFeatures.value = false
|
||||
renderComponent()
|
||||
expect(
|
||||
screen.queryByTestId('subscribe-button-mock')
|
||||
|
||||
@@ -30,7 +30,10 @@
|
||||
</div>
|
||||
|
||||
<!-- Credits Section -->
|
||||
<div v-if="isActiveSubscription" class="flex items-center gap-2 px-4 py-2">
|
||||
<div
|
||||
v-if="canAccessSubscriptionFeatures"
|
||||
class="flex items-center gap-2 px-4 py-2"
|
||||
>
|
||||
<i class="icon-[lucide--component] text-sm text-amber-400" />
|
||||
<Skeleton v-if="isLoading" width="4rem" height="1.25rem" class="w-full" />
|
||||
<span v-else class="text-base font-semibold text-base-foreground">{{
|
||||
@@ -80,7 +83,7 @@
|
||||
<Divider class="mx-0 my-2" />
|
||||
|
||||
<div
|
||||
v-if="isActiveSubscription"
|
||||
v-if="canAccessSubscriptionFeatures"
|
||||
class="flex cursor-pointer items-center gap-2 px-4 py-2 hover:bg-secondary-background-hover"
|
||||
data-testid="partner-nodes-menu-item"
|
||||
@click="handleOpenPartnerNodesInfo"
|
||||
@@ -110,7 +113,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isActiveSubscription"
|
||||
v-if="canAccessSubscriptionFeatures"
|
||||
class="flex cursor-pointer items-center gap-2 px-4 py-2 hover:bg-secondary-background-hover"
|
||||
data-testid="manage-plan-menu-item"
|
||||
@click="handleOpenPlanAndCreditsSettings"
|
||||
@@ -178,7 +181,7 @@ const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
|
||||
const settingsDialog = useSettingsDialog()
|
||||
const dialogService = useDialogService()
|
||||
const {
|
||||
isActiveSubscription,
|
||||
canAccessSubscriptionFeatures,
|
||||
isFreeTier,
|
||||
tier,
|
||||
subscription,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { FirebaseError } from 'firebase/app'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@@ -8,7 +7,10 @@ import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workfl
|
||||
type ModifiedWorkflow = Pick<ComfyWorkflow, 'path' | 'isModified'>
|
||||
|
||||
const mockAuthStore = vi.hoisted(() => ({
|
||||
logout: vi.fn().mockResolvedValue(undefined)
|
||||
logout: vi.fn().mockResolvedValue(undefined),
|
||||
initiateCreditPurchase: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ checkout_url: 'https://checkout.example.com' })
|
||||
}))
|
||||
|
||||
const mockToastStore = vi.hoisted(() => ({
|
||||
@@ -27,28 +29,21 @@ const mockDialogService = vi.hoisted(() => ({
|
||||
confirm: vi.fn()
|
||||
}))
|
||||
|
||||
const mockToastErrorHandler = vi.hoisted(() => vi.fn())
|
||||
|
||||
const knownAuthErrorCodes = new Set([
|
||||
'auth/invalid-credential',
|
||||
'auth/email-already-in-use'
|
||||
])
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
t: (key: string, values?: { workflow?: string }) =>
|
||||
values?.workflow ? `${key}:${values.workflow}` : key,
|
||||
st: (key: string, fallback: string) => {
|
||||
const code = key.replace('auth.errors.', '')
|
||||
return knownAuthErrorCodes.has(code) ? key : fallback
|
||||
}
|
||||
values?.workflow ? `${key}:${values.workflow}` : key
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
isCloud: false
|
||||
}))
|
||||
|
||||
const mockStartTopupTracking = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/platform/telemetry', () => ({
|
||||
useTelemetry: vi.fn(() => undefined)
|
||||
useTelemetry: vi.fn(() => ({
|
||||
startTopupTracking: mockStartTopupTracking
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/updates/common/toastStore', () => ({
|
||||
@@ -71,9 +66,13 @@ vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: vi.fn(() => mockAuthStore)
|
||||
}))
|
||||
|
||||
const mockCanAccessSubscriptionFeatures = vi.hoisted(() => ({
|
||||
value: false
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
useBillingContext: vi.fn(() => ({
|
||||
isActiveSubscription: { value: false },
|
||||
canAccessSubscriptionFeatures: mockCanAccessSubscriptionFeatures,
|
||||
isFreeTier: { value: true },
|
||||
type: { value: 'free' }
|
||||
}))
|
||||
@@ -84,7 +83,7 @@ vi.mock('@/composables/useErrorHandling', () => ({
|
||||
wrapWithErrorHandlingAsync: <TArgs extends unknown[], TReturn>(
|
||||
action: (...args: TArgs) => Promise<TReturn> | TReturn
|
||||
) => action,
|
||||
toastErrorHandler: mockToastErrorHandler
|
||||
toastErrorHandler: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -206,79 +205,39 @@ describe('useAuthActions.logout', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('useAuthActions.reportError', () => {
|
||||
describe('useAuthActions.purchaseCredits', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
mockCanAccessSubscriptionFeatures.value = false
|
||||
})
|
||||
|
||||
it('shows the friendly message for a known Firebase auth code', () => {
|
||||
const { reportError } = useAuthActions()
|
||||
it('returns early when canAccessSubscriptionFeatures is false', async () => {
|
||||
mockCanAccessSubscriptionFeatures.value = false
|
||||
|
||||
reportError(new FirebaseError('auth/invalid-credential', 'raw firebase'))
|
||||
const { purchaseCredits } = useAuthActions()
|
||||
await purchaseCredits(10)
|
||||
|
||||
expect(mockToastStore.add).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'g.error',
|
||||
detail: 'auth.errors.auth/invalid-credential'
|
||||
expect(mockAuthStore.initiateCreditPurchase).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('initiates credit purchase when canAccessSubscriptionFeatures is true', async () => {
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
const mockOpen = vi.spyOn(window, 'open').mockImplementation(() => null)
|
||||
|
||||
const { purchaseCredits } = useAuthActions()
|
||||
await purchaseCredits(10)
|
||||
|
||||
expect(mockAuthStore.initiateCreditPurchase).toHaveBeenCalledWith({
|
||||
amount_micros: 10_000_000,
|
||||
currency: 'usd'
|
||||
})
|
||||
expect(mockToastErrorHandler).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows the signupBlocked message when the error carries the signup_blocked token', () => {
|
||||
const { reportError } = useAuthActions()
|
||||
|
||||
// The backend wraps the rejection in a generic code; we match the token in
|
||||
// the message, so it must win over the auth.errors.${code} fallback.
|
||||
reportError(
|
||||
new FirebaseError(
|
||||
'auth/internal-error',
|
||||
'Account creation is temporarily unavailable. (ref: signup_blocked)'
|
||||
)
|
||||
expect(mockStartTopupTracking).toHaveBeenCalled()
|
||||
expect(mockOpen).toHaveBeenCalledWith(
|
||||
'https://checkout.example.com',
|
||||
'_blank'
|
||||
)
|
||||
|
||||
expect(mockToastStore.add).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'g.error',
|
||||
detail: 'auth.errors.signupBlocked'
|
||||
})
|
||||
expect(mockToastErrorHandler).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('matches the signup_blocked token case-insensitively', () => {
|
||||
const { reportError } = useAuthActions()
|
||||
|
||||
reportError(
|
||||
new FirebaseError('auth/internal-error', 'rejected: SIGNUP_BLOCKED')
|
||||
)
|
||||
|
||||
expect(mockToastStore.add).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'g.error',
|
||||
detail: 'auth.errors.signupBlocked'
|
||||
})
|
||||
})
|
||||
|
||||
it('shows the generic fallback for an unknown Firebase auth code', () => {
|
||||
const { reportError } = useAuthActions()
|
||||
|
||||
reportError(new FirebaseError('auth/some-new-code', 'raw firebase'))
|
||||
|
||||
expect(mockToastStore.add).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'g.error',
|
||||
detail: 'auth.errors.generic'
|
||||
})
|
||||
expect(mockToastErrorHandler).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('delegates non-Firebase errors to toastErrorHandler', () => {
|
||||
const { reportError } = useAuthActions()
|
||||
const networkError = new TypeError('Failed to fetch')
|
||||
|
||||
reportError(networkError)
|
||||
|
||||
expect(mockToastErrorHandler).toHaveBeenCalledWith(networkError)
|
||||
expect(mockToastStore.add).not.toHaveBeenCalled()
|
||||
mockOpen.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -131,8 +131,8 @@ export const useAuthActions = () => {
|
||||
)
|
||||
|
||||
const purchaseCredits = wrapWithErrorHandlingAsync(async (amount: number) => {
|
||||
const { isActiveSubscription } = useBillingContext()
|
||||
if (!isActiveSubscription.value) return
|
||||
const { canAccessSubscriptionFeatures } = useBillingContext()
|
||||
if (!canAccessSubscriptionFeatures.value) return
|
||||
|
||||
const response = await authStore.initiateCreditPurchase({
|
||||
amount_micros: usdToMicros(amount),
|
||||
|
||||
@@ -92,7 +92,7 @@ export interface BillingState {
|
||||
currentTeamCreditStop: ComputedRef<CurrentTeamCreditStop | null>
|
||||
isLoading: Ref<boolean>
|
||||
error: Ref<string | null>
|
||||
isActiveSubscription: ComputedRef<boolean>
|
||||
canAccessSubscriptionFeatures: ComputedRef<boolean>
|
||||
/** Reflects the active workspace's tier, not the user's personal tier. */
|
||||
isFreeTier: ComputedRef<boolean>
|
||||
/** Coarse funding state (`billing_status`); legacy reports null. */
|
||||
|
||||
@@ -84,7 +84,7 @@ vi.mock('@/platform/workspace/stores/teamWorkspaceStore', () => ({
|
||||
|
||||
vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({
|
||||
useSubscription: () => ({
|
||||
isActiveSubscription: { value: true },
|
||||
canAccessSubscriptionFeatures: { value: true },
|
||||
subscriptionTier: { value: 'PRO' },
|
||||
subscriptionDuration: { value: 'MONTHLY' },
|
||||
subscriptionStatus: {
|
||||
@@ -245,9 +245,9 @@ describe('useBillingContext', () => {
|
||||
await expect(topup(99.5)).rejects.toThrow()
|
||||
})
|
||||
|
||||
it('provides isActiveSubscription convenience computed', () => {
|
||||
const { isActiveSubscription } = useBillingContext()
|
||||
expect(isActiveSubscription.value).toBe(true)
|
||||
it('provides canAccessSubscriptionFeatures convenience computed', () => {
|
||||
const { canAccessSubscriptionFeatures } = useBillingContext()
|
||||
expect(canAccessSubscriptionFeatures.value).toBe(true)
|
||||
})
|
||||
|
||||
it('exposes requireActiveSubscription action', async () => {
|
||||
|
||||
@@ -132,8 +132,8 @@ function useBillingContextInternal(): BillingContext {
|
||||
toValue(activeContext.value.currentTeamCreditStop)
|
||||
)
|
||||
|
||||
const isActiveSubscription = computed(() =>
|
||||
toValue(activeContext.value.isActiveSubscription)
|
||||
const canAccessSubscriptionFeatures = computed(() =>
|
||||
toValue(activeContext.value.canAccessSubscriptionFeatures)
|
||||
)
|
||||
|
||||
const isFreeTier = computed(() => subscription.value?.tier === 'FREE')
|
||||
@@ -141,7 +141,7 @@ function useBillingContextInternal(): BillingContext {
|
||||
const isLegacyTeamPlan = computed(
|
||||
() =>
|
||||
type.value === 'workspace' &&
|
||||
isActiveSubscription.value &&
|
||||
canAccessSubscriptionFeatures.value &&
|
||||
!isFreeTier.value &&
|
||||
currentTeamCreditStop.value === null &&
|
||||
(currentPlanSlug.value
|
||||
@@ -296,7 +296,7 @@ function useBillingContextInternal(): BillingContext {
|
||||
currentTeamCreditStop,
|
||||
isLoading,
|
||||
error,
|
||||
isActiveSubscription,
|
||||
canAccessSubscriptionFeatures,
|
||||
isFreeTier,
|
||||
isLegacyTeamPlan,
|
||||
billingStatus,
|
||||
|
||||
340
src/composables/billing/useLegacyBilling.test.ts
Normal file
340
src/composables/billing/useLegacyBilling.test.ts
Normal file
@@ -0,0 +1,340 @@
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useLegacyBilling } from './useLegacyBilling'
|
||||
|
||||
const {
|
||||
mockCanAccessSubscriptionFeatures,
|
||||
mockSubscriptionTier,
|
||||
mockSubscriptionDuration,
|
||||
mockFormattedRenewalDate,
|
||||
mockFormattedEndDate,
|
||||
mockIsCancelled,
|
||||
mockSubscriptionStatus,
|
||||
mockFetchStatus,
|
||||
mockManageSubscription,
|
||||
mockSubscribe,
|
||||
mockShowSubscriptionDialog,
|
||||
mockBalance,
|
||||
mockFetchBalance
|
||||
} = vi.hoisted(() => ({
|
||||
mockCanAccessSubscriptionFeatures: { value: true },
|
||||
mockSubscriptionTier: { value: 'PRO' as string | null },
|
||||
mockSubscriptionDuration: { value: 'MONTHLY' as string | null },
|
||||
mockFormattedRenewalDate: { value: 'Jan 1, 2025' },
|
||||
mockFormattedEndDate: { value: '' },
|
||||
mockIsCancelled: { value: false },
|
||||
mockSubscriptionStatus: {
|
||||
value: { renewal_date: 'Jan 1, 2025', end_date: null } as {
|
||||
renewal_date?: string | null
|
||||
end_date?: string | null
|
||||
} | null
|
||||
},
|
||||
mockFetchStatus: vi.fn().mockResolvedValue(undefined),
|
||||
mockManageSubscription: vi.fn().mockResolvedValue(undefined),
|
||||
mockSubscribe: vi.fn().mockResolvedValue(undefined),
|
||||
mockShowSubscriptionDialog: vi.fn(),
|
||||
mockBalance: {
|
||||
value: {
|
||||
amount_micros: 5000000,
|
||||
currency: 'usd',
|
||||
effective_balance_micros: 5000000,
|
||||
prepaid_balance_micros: 0,
|
||||
cloud_credit_balance_micros: 0
|
||||
} as {
|
||||
amount_micros?: number
|
||||
currency?: string
|
||||
effective_balance_micros?: number
|
||||
prepaid_balance_micros?: number
|
||||
cloud_credit_balance_micros?: number
|
||||
} | null
|
||||
},
|
||||
mockFetchBalance: vi.fn().mockResolvedValue({ amount_micros: 5000000 })
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({
|
||||
useSubscription: () => ({
|
||||
canAccessSubscriptionFeatures: mockCanAccessSubscriptionFeatures,
|
||||
subscriptionTier: mockSubscriptionTier,
|
||||
subscriptionDuration: mockSubscriptionDuration,
|
||||
formattedRenewalDate: mockFormattedRenewalDate,
|
||||
formattedEndDate: mockFormattedEndDate,
|
||||
isCancelled: mockIsCancelled,
|
||||
subscriptionStatus: mockSubscriptionStatus,
|
||||
fetchStatus: mockFetchStatus,
|
||||
manageSubscription: mockManageSubscription,
|
||||
subscribe: mockSubscribe,
|
||||
showSubscriptionDialog: mockShowSubscriptionDialog
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/authStore', () => ({
|
||||
useAuthStore: () => ({
|
||||
get balance() {
|
||||
return mockBalance.value
|
||||
},
|
||||
fetchBalance: mockFetchBalance
|
||||
})
|
||||
}))
|
||||
|
||||
describe('useLegacyBilling', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
vi.clearAllMocks()
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
mockSubscriptionTier.value = 'PRO'
|
||||
mockSubscriptionDuration.value = 'MONTHLY'
|
||||
mockFormattedRenewalDate.value = 'Jan 1, 2025'
|
||||
mockFormattedEndDate.value = ''
|
||||
mockIsCancelled.value = false
|
||||
mockSubscriptionStatus.value = {
|
||||
renewal_date: 'Jan 1, 2025',
|
||||
end_date: null
|
||||
}
|
||||
mockBalance.value = {
|
||||
amount_micros: 5000000,
|
||||
currency: 'usd',
|
||||
effective_balance_micros: 5000000,
|
||||
prepaid_balance_micros: 0,
|
||||
cloud_credit_balance_micros: 0
|
||||
}
|
||||
})
|
||||
|
||||
describe('canAccessSubscriptionFeatures', () => {
|
||||
it('returns true when subscription can access features', () => {
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
const { canAccessSubscriptionFeatures } = useLegacyBilling()
|
||||
expect(canAccessSubscriptionFeatures.value).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when subscription cannot access features', () => {
|
||||
mockCanAccessSubscriptionFeatures.value = false
|
||||
const { canAccessSubscriptionFeatures } = useLegacyBilling()
|
||||
expect(canAccessSubscriptionFeatures.value).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isFreeTier', () => {
|
||||
it('returns true when subscription tier is FREE', () => {
|
||||
mockSubscriptionTier.value = 'FREE'
|
||||
const { isFreeTier } = useLegacyBilling()
|
||||
expect(isFreeTier.value).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when subscription tier is not FREE', () => {
|
||||
mockSubscriptionTier.value = 'PRO'
|
||||
const { isFreeTier } = useLegacyBilling()
|
||||
expect(isFreeTier.value).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('subscription', () => {
|
||||
it('returns subscription info when active', () => {
|
||||
const { subscription } = useLegacyBilling()
|
||||
expect(subscription.value).toEqual({
|
||||
isActive: true,
|
||||
tier: 'PRO',
|
||||
duration: 'MONTHLY',
|
||||
planSlug: null,
|
||||
renewalDate: 'Jan 1, 2025',
|
||||
endDate: null,
|
||||
isCancelled: false,
|
||||
hasFunds: true
|
||||
})
|
||||
})
|
||||
|
||||
it('returns null when no subscription and no tier', () => {
|
||||
mockCanAccessSubscriptionFeatures.value = false
|
||||
mockSubscriptionTier.value = null
|
||||
const { subscription } = useLegacyBilling()
|
||||
expect(subscription.value).toBeNull()
|
||||
})
|
||||
|
||||
it('returns subscription with endDate when cancelled', () => {
|
||||
mockIsCancelled.value = true
|
||||
mockSubscriptionStatus.value = {
|
||||
renewal_date: 'Jan 1, 2025',
|
||||
end_date: 'Feb 1, 2025'
|
||||
}
|
||||
const { subscription } = useLegacyBilling()
|
||||
expect(subscription.value?.isCancelled).toBe(true)
|
||||
expect(subscription.value?.endDate).toBe('Feb 1, 2025')
|
||||
})
|
||||
|
||||
it('returns hasFunds false when balance is zero', () => {
|
||||
mockBalance.value = { amount_micros: 0 }
|
||||
const { subscription } = useLegacyBilling()
|
||||
expect(subscription.value?.hasFunds).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('balance', () => {
|
||||
it('returns balance info from auth store', () => {
|
||||
const { balance } = useLegacyBilling()
|
||||
expect(balance.value).toEqual({
|
||||
amountMicros: 5000000,
|
||||
currency: 'usd',
|
||||
effectiveBalanceMicros: 5000000,
|
||||
prepaidBalanceMicros: 0,
|
||||
cloudCreditBalanceMicros: 0
|
||||
})
|
||||
})
|
||||
|
||||
it('returns null when no balance', () => {
|
||||
mockBalance.value = null
|
||||
const { balance } = useLegacyBilling()
|
||||
expect(balance.value).toBeNull()
|
||||
})
|
||||
|
||||
it('handles missing optional balance fields', () => {
|
||||
mockBalance.value = { amount_micros: 1000 }
|
||||
const { balance } = useLegacyBilling()
|
||||
expect(balance.value).toEqual({
|
||||
amountMicros: 1000,
|
||||
currency: 'usd',
|
||||
effectiveBalanceMicros: 1000,
|
||||
prepaidBalanceMicros: 0,
|
||||
cloudCreditBalanceMicros: 0
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('plans', () => {
|
||||
it('returns empty array (legacy has no plans)', () => {
|
||||
const { plans } = useLegacyBilling()
|
||||
expect(plans.value).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('currentPlanSlug', () => {
|
||||
it('returns null (legacy has no plan slugs)', () => {
|
||||
const { currentPlanSlug } = useLegacyBilling()
|
||||
expect(currentPlanSlug.value).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('initialize', () => {
|
||||
it('fetches status and balance', async () => {
|
||||
const { initialize } = useLegacyBilling()
|
||||
await initialize()
|
||||
expect(mockFetchStatus).toHaveBeenCalled()
|
||||
expect(mockFetchBalance).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not re-initialize if already initialized', async () => {
|
||||
const { initialize } = useLegacyBilling()
|
||||
await initialize()
|
||||
await initialize()
|
||||
expect(mockFetchStatus).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('re-fetches balance for free tier with zero balance', async () => {
|
||||
mockSubscriptionTier.value = 'FREE'
|
||||
mockBalance.value = { amount_micros: 0 }
|
||||
const { initialize } = useLegacyBilling()
|
||||
await initialize()
|
||||
expect(mockFetchBalance).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('sets error on failure', async () => {
|
||||
mockFetchStatus.mockRejectedValueOnce(new Error('Network error'))
|
||||
const { initialize, error } = useLegacyBilling()
|
||||
await expect(initialize()).rejects.toThrow('Network error')
|
||||
expect(error.value).toBe('Network error')
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchStatus', () => {
|
||||
it('calls underlying fetchStatus', async () => {
|
||||
const { fetchStatus } = useLegacyBilling()
|
||||
await fetchStatus()
|
||||
expect(mockFetchStatus).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('sets error on failure', async () => {
|
||||
mockFetchStatus.mockRejectedValueOnce(new Error('Fetch failed'))
|
||||
const { fetchStatus, error } = useLegacyBilling()
|
||||
await expect(fetchStatus()).rejects.toThrow('Fetch failed')
|
||||
expect(error.value).toBe('Fetch failed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchBalance', () => {
|
||||
it('calls auth store fetchBalance', async () => {
|
||||
const { fetchBalance } = useLegacyBilling()
|
||||
await fetchBalance()
|
||||
expect(mockFetchBalance).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('sets error on failure', async () => {
|
||||
mockFetchBalance.mockRejectedValueOnce(new Error('Balance fetch failed'))
|
||||
const { fetchBalance, error } = useLegacyBilling()
|
||||
await expect(fetchBalance()).rejects.toThrow('Balance fetch failed')
|
||||
expect(error.value).toBe('Balance fetch failed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('subscribe', () => {
|
||||
it('calls legacy subscribe', async () => {
|
||||
const { subscribe } = useLegacyBilling()
|
||||
await subscribe('pro-monthly')
|
||||
expect(mockSubscribe).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('previewSubscribe', () => {
|
||||
it('returns null (legacy does not support preview)', async () => {
|
||||
const { previewSubscribe } = useLegacyBilling()
|
||||
const result = await previewSubscribe('pro-monthly')
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('manageSubscription', () => {
|
||||
it('calls legacy manageSubscription', async () => {
|
||||
const { manageSubscription } = useLegacyBilling()
|
||||
await manageSubscription()
|
||||
expect(mockManageSubscription).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('cancelSubscription', () => {
|
||||
it('calls legacy manageSubscription', async () => {
|
||||
const { cancelSubscription } = useLegacyBilling()
|
||||
await cancelSubscription()
|
||||
expect(mockManageSubscription).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchPlans', () => {
|
||||
it('does nothing (legacy has no plans)', async () => {
|
||||
const { fetchPlans } = useLegacyBilling()
|
||||
await fetchPlans()
|
||||
// No error should be thrown
|
||||
})
|
||||
})
|
||||
|
||||
describe('requireActiveSubscription', () => {
|
||||
it('does not show dialog when subscription can access features', async () => {
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
const { requireActiveSubscription } = useLegacyBilling()
|
||||
await requireActiveSubscription()
|
||||
expect(mockShowSubscriptionDialog).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows dialog when subscription cannot access features', async () => {
|
||||
mockCanAccessSubscriptionFeatures.value = false
|
||||
const { requireActiveSubscription } = useLegacyBilling()
|
||||
await requireActiveSubscription()
|
||||
expect(mockShowSubscriptionDialog).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('showSubscriptionDialog', () => {
|
||||
it('calls legacy showSubscriptionDialog', () => {
|
||||
const { showSubscriptionDialog } = useLegacyBilling()
|
||||
showSubscriptionDialog()
|
||||
expect(mockShowSubscriptionDialog).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -26,7 +26,7 @@ import type {
|
||||
*/
|
||||
export function useLegacyBilling(): BillingState & BillingActions {
|
||||
const {
|
||||
isActiveSubscription: legacyIsActiveSubscription,
|
||||
canAccessSubscriptionFeatures: legacyCanAccessSubscriptionFeatures,
|
||||
subscriptionTier,
|
||||
subscriptionDuration,
|
||||
subscriptionStatus: legacySubscriptionStatus,
|
||||
@@ -44,16 +44,18 @@ export function useLegacyBilling(): BillingState & BillingActions {
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const isActiveSubscription = computed(() => legacyIsActiveSubscription.value)
|
||||
const canAccessSubscriptionFeatures = computed(
|
||||
() => legacyCanAccessSubscriptionFeatures.value
|
||||
)
|
||||
const isFreeTier = computed(() => subscriptionTier.value === 'FREE')
|
||||
|
||||
const subscription = computed<SubscriptionInfo | null>(() => {
|
||||
if (!legacyIsActiveSubscription.value && !subscriptionTier.value) {
|
||||
if (!legacyCanAccessSubscriptionFeatures.value && !subscriptionTier.value) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
isActive: legacyIsActiveSubscription.value,
|
||||
isActive: legacyCanAccessSubscriptionFeatures.value,
|
||||
tier: subscriptionTier.value,
|
||||
duration: subscriptionDuration.value,
|
||||
planSlug: null, // Legacy doesn't use plan slugs
|
||||
@@ -84,7 +86,7 @@ export function useLegacyBilling(): BillingState & BillingActions {
|
||||
const billingStatus = computed<BillingStatus | null>(() => null)
|
||||
const subscriptionStatus = computed<BillingSubscriptionStatus | null>(() => {
|
||||
if (isCancelled.value) return 'canceled'
|
||||
if (legacyIsActiveSubscription.value) return 'active'
|
||||
if (legacyCanAccessSubscriptionFeatures.value) return 'active'
|
||||
return null
|
||||
})
|
||||
const tier = computed(() => subscriptionTier.value)
|
||||
@@ -188,7 +190,7 @@ export function useLegacyBilling(): BillingState & BillingActions {
|
||||
|
||||
async function requireActiveSubscription(): Promise<void> {
|
||||
await fetchStatus()
|
||||
if (!isActiveSubscription.value) {
|
||||
if (!canAccessSubscriptionFeatures.value) {
|
||||
legacyShowSubscriptionDialog()
|
||||
}
|
||||
}
|
||||
@@ -208,7 +210,7 @@ export function useLegacyBilling(): BillingState & BillingActions {
|
||||
currentTeamCreditStop,
|
||||
isLoading,
|
||||
error,
|
||||
isActiveSubscription,
|
||||
canAccessSubscriptionFeatures,
|
||||
isFreeTier,
|
||||
billingStatus,
|
||||
subscriptionStatus,
|
||||
|
||||
@@ -5,13 +5,11 @@ import { ref } from 'vue'
|
||||
import { useCoreCommands } from '@/composables/useCoreCommands'
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import type * as ModelStoreModule from '@/stores/modelStore'
|
||||
import { createMockLGraphNode } from '@/utils/__tests__/litegraphTestUtils'
|
||||
import { fromPartial } from '@total-typescript/shoehorn'
|
||||
|
||||
// Mock vue-i18n for useExternalLink
|
||||
const mockLocale = ref('en')
|
||||
@@ -55,6 +53,7 @@ vi.mock('@/scripts/app', () => {
|
||||
}),
|
||||
openClipspace: vi.fn(),
|
||||
refreshComboInNodes: vi.fn().mockResolvedValue(undefined),
|
||||
queuePrompt: vi.fn().mockResolvedValue(undefined),
|
||||
canvas: mockCanvas,
|
||||
rootGraph: {
|
||||
clear: mockGraphClear
|
||||
@@ -116,7 +115,9 @@ vi.mock('@/services/litegraphService', () => ({
|
||||
const mockTrackHelpResourceClicked = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/platform/telemetry', () => ({
|
||||
useTelemetry: vi.fn(() => ({
|
||||
trackHelpResourceClicked: mockTrackHelpResourceClicked
|
||||
trackHelpResourceClicked: mockTrackHelpResourceClicked,
|
||||
trackRunButton: vi.fn(),
|
||||
trackWorkflowExecution: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -137,23 +138,6 @@ vi.mock('@/stores/toastStore', () => ({
|
||||
useToastStore: vi.fn(() => ({}))
|
||||
}))
|
||||
|
||||
const mockToastAdd = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/platform/updates/common/toastStore', () => ({
|
||||
useToastStore: vi.fn(() => ({ add: mockToastAdd }))
|
||||
}))
|
||||
|
||||
const mockAssetBrowse = vi.hoisted(() =>
|
||||
vi.fn<(options: { onAssetSelected?: (asset: AssetItem) => void }) => void>()
|
||||
)
|
||||
vi.mock('@/platform/assets/composables/useAssetBrowserDialog', () => ({
|
||||
useAssetBrowserDialog: vi.fn(() => ({ browse: mockAssetBrowse }))
|
||||
}))
|
||||
|
||||
const mockStartModelNodeDrag = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/composables/node/startModelNodeDragFromAsset', () => ({
|
||||
startModelNodeDragFromAsset: mockStartModelNodeDrag
|
||||
}))
|
||||
|
||||
const mockChangeTracker = vi.hoisted(() => ({
|
||||
captureCanvasState: vi.fn()
|
||||
}))
|
||||
@@ -190,15 +174,21 @@ vi.mock('@/composables/auth/useAuthActions', () => ({
|
||||
|
||||
vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({
|
||||
useSubscription: vi.fn(() => ({
|
||||
isActiveSubscription: vi.fn().mockReturnValue(true),
|
||||
canAccessSubscriptionFeatures: vi.fn().mockReturnValue(true),
|
||||
showSubscriptionDialog: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
const { mockCanAccessSubscriptionFeatures, mockShowSubscriptionDialog } =
|
||||
vi.hoisted(() => ({
|
||||
mockCanAccessSubscriptionFeatures: { value: true },
|
||||
mockShowSubscriptionDialog: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
useBillingContext: vi.fn(() => ({
|
||||
isActiveSubscription: { value: true },
|
||||
showSubscriptionDialog: vi.fn()
|
||||
canAccessSubscriptionFeatures: mockCanAccessSubscriptionFeatures,
|
||||
showSubscriptionDialog: mockShowSubscriptionDialog
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -298,6 +288,7 @@ describe('useCoreCommands', () => {
|
||||
|
||||
// Reset app state
|
||||
app.canvas.subgraph = undefined
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
|
||||
// Mock settings store
|
||||
vi.mocked(useSettingStore).mockReturnValue(createMockSettingStore(false))
|
||||
@@ -638,46 +629,52 @@ describe('useCoreCommands', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('BrowseModelAssets command', () => {
|
||||
const asset = fromPartial<AssetItem>({ id: 'asset-1' })
|
||||
describe('Queue commands with subscription check', () => {
|
||||
const findCmd = (id: string) =>
|
||||
useCoreCommands().find((cmd) => cmd.id === id)!
|
||||
|
||||
async function selectAssetFromBrowser() {
|
||||
vi.mocked(useSettingStore).mockReturnValue(createMockSettingStore(true))
|
||||
it('Comfy.QueuePrompt shows subscription dialog and skips queue when features inaccessible', async () => {
|
||||
mockCanAccessSubscriptionFeatures.value = false
|
||||
|
||||
const command = useCoreCommands().find(
|
||||
(cmd) => cmd.id === 'Comfy.BrowseModelAssets'
|
||||
)!
|
||||
await command.function()
|
||||
await findCmd('Comfy.QueuePrompt').function()
|
||||
|
||||
const { onAssetSelected } = mockAssetBrowse.mock.calls[0][0]
|
||||
onAssetSelected?.(asset)
|
||||
}
|
||||
|
||||
it('starts a model node drag for the selected asset', async () => {
|
||||
mockStartModelNodeDrag.mockReturnValue(undefined)
|
||||
|
||||
await selectAssetFromBrowser()
|
||||
|
||||
expect(mockStartModelNodeDrag).toHaveBeenCalledWith(
|
||||
asset,
|
||||
'asset_browser'
|
||||
)
|
||||
expect(mockToastAdd).not.toHaveBeenCalled()
|
||||
expect(mockShowSubscriptionDialog).toHaveBeenCalled()
|
||||
expect(app.queuePrompt).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows an error toast when the asset cannot start a drag', async () => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
mockStartModelNodeDrag.mockReturnValue({
|
||||
code: 'NO_PROVIDER',
|
||||
message: 'No node provider registered',
|
||||
assetId: 'asset-1'
|
||||
})
|
||||
it('Comfy.QueuePrompt queues prompt when features accessible', async () => {
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
|
||||
await selectAssetFromBrowser()
|
||||
await findCmd('Comfy.QueuePrompt').function()
|
||||
|
||||
expect(mockToastAdd).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ severity: 'error' })
|
||||
)
|
||||
expect(mockShowSubscriptionDialog).not.toHaveBeenCalled()
|
||||
expect(app.queuePrompt).toHaveBeenCalledWith(0, expect.any(Number))
|
||||
})
|
||||
|
||||
it('Comfy.QueuePromptFront shows subscription dialog and skips queue when features inaccessible', async () => {
|
||||
mockCanAccessSubscriptionFeatures.value = false
|
||||
|
||||
await findCmd('Comfy.QueuePromptFront').function()
|
||||
|
||||
expect(mockShowSubscriptionDialog).toHaveBeenCalled()
|
||||
expect(app.queuePrompt).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('Comfy.QueuePromptFront queues prompt at front when features accessible', async () => {
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
|
||||
await findCmd('Comfy.QueuePromptFront').function()
|
||||
|
||||
expect(mockShowSubscriptionDialog).not.toHaveBeenCalled()
|
||||
expect(app.queuePrompt).toHaveBeenCalledWith(-1, expect.any(Number))
|
||||
})
|
||||
|
||||
it('Comfy.QueueSelectedOutputNodes shows subscription dialog when features inaccessible', async () => {
|
||||
mockCanAccessSubscriptionFeatures.value = false
|
||||
|
||||
await findCmd('Comfy.QueueSelectedOutputNodes').function()
|
||||
|
||||
expect(mockShowSubscriptionDialog).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -74,7 +74,8 @@ import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const moveSelectedNodesVersionAdded = '1.22.2'
|
||||
export function useCoreCommands(): ComfyCommand[] {
|
||||
const { isActiveSubscription, showSubscriptionDialog } = useBillingContext()
|
||||
const { canAccessSubscriptionFeatures, showSubscriptionDialog } =
|
||||
useBillingContext()
|
||||
const workflowService = useWorkflowService()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const settingsDialog = useSettingsDialog()
|
||||
@@ -502,7 +503,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
trigger_source?: ExecutionTriggerSource
|
||||
}) => {
|
||||
trackRunButton(metadata)
|
||||
if (!isActiveSubscription.value) {
|
||||
if (!canAccessSubscriptionFeatures.value) {
|
||||
showSubscriptionDialog()
|
||||
return
|
||||
}
|
||||
@@ -525,7 +526,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
trigger_source?: ExecutionTriggerSource
|
||||
}) => {
|
||||
trackRunButton(metadata)
|
||||
if (!isActiveSubscription.value) {
|
||||
if (!canAccessSubscriptionFeatures.value) {
|
||||
showSubscriptionDialog()
|
||||
return
|
||||
}
|
||||
@@ -547,7 +548,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
trigger_source?: ExecutionTriggerSource
|
||||
}) => {
|
||||
trackRunButton(metadata)
|
||||
if (!isActiveSubscription.value) {
|
||||
if (!canAccessSubscriptionFeatures.value) {
|
||||
showSubscriptionDialog()
|
||||
return
|
||||
}
|
||||
|
||||
103
src/extensions/core/cloudRemoteConfig.test.ts
Normal file
103
src/extensions/core/cloudRemoteConfig.test.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const {
|
||||
mockIsLoggedIn,
|
||||
mockCanAccessSubscriptionFeatures,
|
||||
mockRefreshRemoteConfig,
|
||||
mockRegisterExtension,
|
||||
mockWatchDebounced
|
||||
} = vi.hoisted(() => ({
|
||||
mockIsLoggedIn: { value: false },
|
||||
mockCanAccessSubscriptionFeatures: { value: true },
|
||||
mockRefreshRemoteConfig: vi.fn(),
|
||||
mockRegisterExtension: vi.fn(),
|
||||
mockWatchDebounced: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@vueuse/core', () => ({
|
||||
watchDebounced: mockWatchDebounced
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/auth/useCurrentUser', () => ({
|
||||
useCurrentUser: () => ({
|
||||
isLoggedIn: mockIsLoggedIn
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
useBillingContext: () => ({
|
||||
canAccessSubscriptionFeatures: mockCanAccessSubscriptionFeatures
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/remoteConfig/refreshRemoteConfig', () => ({
|
||||
refreshRemoteConfig: mockRefreshRemoteConfig
|
||||
}))
|
||||
|
||||
vi.mock('@/services/extensionService', () => ({
|
||||
useExtensionService: () => ({
|
||||
registerExtension: mockRegisterExtension
|
||||
})
|
||||
}))
|
||||
|
||||
describe('cloudRemoteConfig extension', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.resetModules()
|
||||
mockIsLoggedIn.value = false
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
})
|
||||
|
||||
it('registers extension with correct name', async () => {
|
||||
await import('./cloudRemoteConfig')
|
||||
|
||||
expect(mockRegisterExtension).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: 'Comfy.Cloud.RemoteConfig'
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('setup watches isLoggedIn and canAccessSubscriptionFeatures', async () => {
|
||||
await import('./cloudRemoteConfig')
|
||||
|
||||
const registeredExtension = mockRegisterExtension.mock.calls[0][0]
|
||||
await registeredExtension.setup()
|
||||
|
||||
expect(mockWatchDebounced).toHaveBeenCalledWith(
|
||||
[mockIsLoggedIn, mockCanAccessSubscriptionFeatures],
|
||||
expect.any(Function),
|
||||
expect.objectContaining({ debounce: 256, immediate: true })
|
||||
)
|
||||
})
|
||||
|
||||
it('watch callback refreshes config when logged in', async () => {
|
||||
await import('./cloudRemoteConfig')
|
||||
|
||||
const registeredExtension = mockRegisterExtension.mock.calls[0][0]
|
||||
await registeredExtension.setup()
|
||||
|
||||
// Get the callback passed to watchDebounced
|
||||
const watchCallback = mockWatchDebounced.mock.calls[0][1]
|
||||
|
||||
// Simulate logged in state
|
||||
mockIsLoggedIn.value = true
|
||||
watchCallback()
|
||||
|
||||
expect(mockRefreshRemoteConfig).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('watch callback does not refresh when not logged in', async () => {
|
||||
await import('./cloudRemoteConfig')
|
||||
|
||||
const registeredExtension = mockRegisterExtension.mock.calls[0][0]
|
||||
await registeredExtension.setup()
|
||||
|
||||
const watchCallback = mockWatchDebounced.mock.calls[0][1]
|
||||
|
||||
mockIsLoggedIn.value = false
|
||||
watchCallback()
|
||||
|
||||
expect(mockRefreshRemoteConfig).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -14,13 +14,13 @@ useExtensionService().registerExtension({
|
||||
|
||||
setup: async () => {
|
||||
const { isLoggedIn } = useCurrentUser()
|
||||
const { isActiveSubscription } = useBillingContext()
|
||||
const { canAccessSubscriptionFeatures } = useBillingContext()
|
||||
|
||||
// Refresh config when auth or subscription status changes
|
||||
// Primary auth refresh is handled by WorkspaceAuthGate on mount
|
||||
// This watcher handles subscription changes and acts as a backup for auth
|
||||
watchDebounced(
|
||||
[isLoggedIn, isActiveSubscription],
|
||||
[isLoggedIn, canAccessSubscriptionFeatures],
|
||||
() => {
|
||||
if (!isLoggedIn.value) return
|
||||
void refreshRemoteConfig()
|
||||
|
||||
@@ -39,7 +39,7 @@ vi.mock('@/composables/useErrorHandling', () => ({
|
||||
}))
|
||||
|
||||
const subscriptionMocks = vi.hoisted(() => ({
|
||||
isActiveSubscription: { value: false },
|
||||
canAccessSubscriptionFeatures: { value: false },
|
||||
isInitialized: { value: true },
|
||||
subscriptionStatus: { value: null }
|
||||
}))
|
||||
@@ -111,7 +111,7 @@ describe('CloudSubscriptionRedirectView', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockQuery = {}
|
||||
subscriptionMocks.isActiveSubscription.value = false
|
||||
subscriptionMocks.canAccessSubscriptionFeatures.value = false
|
||||
subscriptionMocks.isInitialized.value = true
|
||||
})
|
||||
|
||||
@@ -150,7 +150,7 @@ describe('CloudSubscriptionRedirectView', () => {
|
||||
})
|
||||
|
||||
test('opens billing portal when subscription is already active', async () => {
|
||||
subscriptionMocks.isActiveSubscription.value = true
|
||||
subscriptionMocks.canAccessSubscriptionFeatures.value = true
|
||||
|
||||
await mountView({ tier: 'creator' })
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@ const router = useRouter()
|
||||
const { reportError, accessBillingPortal } = useAuthActions()
|
||||
const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
||||
|
||||
const { isActiveSubscription, isInitialized, initialize } = useBillingContext()
|
||||
const { canAccessSubscriptionFeatures, isInitialized, initialize } =
|
||||
useBillingContext()
|
||||
|
||||
const selectedTierKey = ref<TierKey | null>(null)
|
||||
|
||||
@@ -109,7 +110,7 @@ const runRedirect = wrapWithErrorHandlingAsync(async () => {
|
||||
await initialize()
|
||||
}
|
||||
|
||||
if (isActiveSubscription.value) {
|
||||
if (canAccessSubscriptionFeatures.value) {
|
||||
await accessBillingPortal(undefined, false)
|
||||
} else {
|
||||
await performSubscriptionCheckout(tierKeyParam, billingCycle, false)
|
||||
|
||||
@@ -20,7 +20,7 @@ type TeamStop = CurrentTeamCreditStop
|
||||
const state = vi.hoisted(() => ({
|
||||
balance: null as Balance | null,
|
||||
subscription: null as Subscription | null,
|
||||
isActiveSubscription: false,
|
||||
canAccessSubscriptionFeatures: false,
|
||||
isFreeTier: false,
|
||||
currentTeamCreditStop: null as TeamStop | null,
|
||||
isLoading: false,
|
||||
@@ -53,7 +53,9 @@ vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
useBillingContext: () => ({
|
||||
balance: computed(() => state.balance),
|
||||
subscription: computed(() => state.subscription),
|
||||
isActiveSubscription: computed(() => state.isActiveSubscription),
|
||||
canAccessSubscriptionFeatures: computed(
|
||||
() => state.canAccessSubscriptionFeatures
|
||||
),
|
||||
isFreeTier: computed(() => state.isFreeTier),
|
||||
currentTeamCreditStop: computed(() => state.currentTeamCreditStop),
|
||||
isLoading: computed(() => state.isLoading),
|
||||
@@ -142,7 +144,7 @@ function renderTile(props: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
function activeProSubscription() {
|
||||
state.isActiveSubscription = true
|
||||
state.canAccessSubscriptionFeatures = true
|
||||
state.subscription = {
|
||||
tier: 'PRO',
|
||||
duration: 'MONTHLY',
|
||||
@@ -160,7 +162,7 @@ describe('CreditsTile', () => {
|
||||
beforeEach(() => {
|
||||
state.balance = null
|
||||
state.subscription = null
|
||||
state.isActiveSubscription = false
|
||||
state.canAccessSubscriptionFeatures = false
|
||||
state.isFreeTier = false
|
||||
state.currentTeamCreditStop = null
|
||||
state.isLoading = false
|
||||
@@ -195,7 +197,7 @@ describe('CreditsTile', () => {
|
||||
})
|
||||
|
||||
it('uses the team credit stop monthly grant for the monthly total', () => {
|
||||
state.isActiveSubscription = true
|
||||
state.canAccessSubscriptionFeatures = true
|
||||
state.subscription = {
|
||||
tier: 'TEAM',
|
||||
duration: 'ANNUAL',
|
||||
@@ -214,7 +216,7 @@ describe('CreditsTile', () => {
|
||||
})
|
||||
|
||||
it('uses the per-month nominal grant for an annual personal tier', () => {
|
||||
state.isActiveSubscription = true
|
||||
state.canAccessSubscriptionFeatures = true
|
||||
state.subscription = {
|
||||
tier: 'PRO',
|
||||
duration: 'ANNUAL',
|
||||
@@ -262,7 +264,7 @@ describe('CreditsTile', () => {
|
||||
})
|
||||
|
||||
it('shows only the balance with no breakdown when there is no active subscription', () => {
|
||||
state.isActiveSubscription = false
|
||||
state.canAccessSubscriptionFeatures = false
|
||||
state.balance = { amountMicros: 500 }
|
||||
const { container } = renderTile()
|
||||
expect(container.textContent).toContain('1,055')
|
||||
|
||||
@@ -206,7 +206,7 @@ const { locale, t } = useI18n()
|
||||
const {
|
||||
subscription,
|
||||
balance,
|
||||
isActiveSubscription,
|
||||
canAccessSubscriptionFeatures,
|
||||
isFreeTier,
|
||||
currentTeamCreditStop,
|
||||
fetchBalance,
|
||||
@@ -299,7 +299,9 @@ const monthlyUsageLabel = computed(() =>
|
||||
})
|
||||
)
|
||||
|
||||
const showBreakdown = computed(() => isActiveSubscription.value && !zeroState)
|
||||
const showBreakdown = computed(
|
||||
() => canAccessSubscriptionFeatures.value && !zeroState
|
||||
)
|
||||
const showBar = computed(
|
||||
() =>
|
||||
showBreakdown.value &&
|
||||
@@ -307,7 +309,10 @@ const showBar = computed(
|
||||
monthlyTotalCredits.value > 0
|
||||
)
|
||||
const showActionButton = computed(
|
||||
() => isActiveSubscription.value && !zeroState && permissions.value.canTopUp
|
||||
() =>
|
||||
canAccessSubscriptionFeatures.value &&
|
||||
!zeroState &&
|
||||
permissions.value.canTopUp
|
||||
)
|
||||
|
||||
const isMonthlyDepleted = computed(
|
||||
|
||||
@@ -23,7 +23,7 @@ function createDeferredPromise<T>() {
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
const mockIsActiveSubscription = ref(false)
|
||||
const mockCanAccessSubscriptionFeatures = ref(false)
|
||||
const mockSubscriptionTier = ref<SubscriptionTier | null>(null)
|
||||
const mockSubscriptionDuration = ref<'MONTHLY' | 'ANNUAL'>('MONTHLY')
|
||||
const mockAccessBillingPortal = vi.fn()
|
||||
@@ -66,13 +66,15 @@ Object.defineProperty(globalThis, 'localStorage', {
|
||||
|
||||
vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
useBillingContext: () => ({
|
||||
isActiveSubscription: computed(() => mockIsActiveSubscription.value),
|
||||
canAccessSubscriptionFeatures: computed(
|
||||
() => mockCanAccessSubscriptionFeatures.value
|
||||
),
|
||||
isFreeTier: computed(() => mockSubscriptionTier.value === 'FREE'),
|
||||
tier: computed(() => mockSubscriptionTier.value),
|
||||
subscription: computed(() =>
|
||||
mockSubscriptionTier.value
|
||||
? {
|
||||
isActive: mockIsActiveSubscription.value,
|
||||
isActive: mockCanAccessSubscriptionFeatures.value,
|
||||
tier: mockSubscriptionTier.value,
|
||||
duration: mockSubscriptionDuration.value,
|
||||
planSlug: null,
|
||||
@@ -226,7 +228,7 @@ const onChooseTeamWorkspace = vi.fn()
|
||||
describe('PricingTable', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsActiveSubscription.value = false
|
||||
mockCanAccessSubscriptionFeatures.value = false
|
||||
mockSubscriptionTier.value = null
|
||||
mockSubscriptionDuration.value = 'MONTHLY'
|
||||
mockUserId.value = 'user-123'
|
||||
@@ -242,7 +244,7 @@ describe('PricingTable', () => {
|
||||
|
||||
describe('billing portal deep linking', () => {
|
||||
it('should call accessBillingPortal with yearly tier suffix when billing cycle is yearly (default)', async () => {
|
||||
mockIsActiveSubscription.value = true
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
mockSubscriptionTier.value = 'STANDARD'
|
||||
|
||||
renderComponent()
|
||||
@@ -267,7 +269,7 @@ describe('PricingTable', () => {
|
||||
})
|
||||
|
||||
it('should call accessBillingPortal with different tiers correctly', async () => {
|
||||
mockIsActiveSubscription.value = true
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
mockSubscriptionTier.value = 'STANDARD'
|
||||
|
||||
renderComponent()
|
||||
@@ -284,7 +286,7 @@ describe('PricingTable', () => {
|
||||
})
|
||||
|
||||
it('records the plan snapshot that was actually opened', async () => {
|
||||
mockIsActiveSubscription.value = true
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
mockSubscriptionTier.value = 'STANDARD'
|
||||
|
||||
const portalOpen = createDeferredPromise<boolean>()
|
||||
@@ -324,7 +326,7 @@ describe('PricingTable', () => {
|
||||
})
|
||||
|
||||
it('does not record a pending upgrade when the billing portal does not open', async () => {
|
||||
mockIsActiveSubscription.value = true
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
mockSubscriptionTier.value = 'STANDARD'
|
||||
mockAccessBillingPortal.mockResolvedValueOnce(false)
|
||||
|
||||
@@ -344,7 +346,7 @@ describe('PricingTable', () => {
|
||||
})
|
||||
|
||||
it('should use the latest userId value when it changes after mount', async () => {
|
||||
mockIsActiveSubscription.value = true
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
mockSubscriptionTier.value = 'STANDARD'
|
||||
mockUserId.value = 'user-early'
|
||||
|
||||
@@ -371,7 +373,7 @@ describe('PricingTable', () => {
|
||||
})
|
||||
|
||||
it('should not call accessBillingPortal when clicking current plan', async () => {
|
||||
mockIsActiveSubscription.value = true
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
mockSubscriptionTier.value = 'CREATOR'
|
||||
mockSubscriptionDuration.value = 'ANNUAL'
|
||||
|
||||
@@ -391,7 +393,7 @@ describe('PricingTable', () => {
|
||||
})
|
||||
|
||||
it('does not highlight a current plan when the facade duration differs from the selected cycle', async () => {
|
||||
mockIsActiveSubscription.value = true
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
mockSubscriptionTier.value = 'CREATOR'
|
||||
mockSubscriptionDuration.value = 'MONTHLY'
|
||||
|
||||
@@ -406,7 +408,7 @@ describe('PricingTable', () => {
|
||||
})
|
||||
|
||||
it('should initiate checkout instead of billing portal for new subscribers', async () => {
|
||||
mockIsActiveSubscription.value = false
|
||||
mockCanAccessSubscriptionFeatures.value = false
|
||||
|
||||
const windowOpenSpy = vi
|
||||
.spyOn(window, 'open')
|
||||
@@ -436,7 +438,7 @@ describe('PricingTable', () => {
|
||||
})
|
||||
|
||||
it('should pass correct tier for each subscription level', async () => {
|
||||
mockIsActiveSubscription.value = true
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
mockSubscriptionTier.value = 'PRO'
|
||||
|
||||
renderComponent()
|
||||
|
||||
@@ -362,7 +362,7 @@ const tiers: PricingTierConfig[] = [
|
||||
}
|
||||
]
|
||||
const {
|
||||
isActiveSubscription,
|
||||
canAccessSubscriptionFeatures,
|
||||
isFreeTier,
|
||||
tier: subscriptionTier,
|
||||
subscription
|
||||
@@ -382,7 +382,7 @@ const popover = ref()
|
||||
const currentBillingCycle = ref<BillingCycle>('yearly')
|
||||
|
||||
const hasPaidSubscription = computed(
|
||||
() => isActiveSubscription.value && !isFreeTier.value
|
||||
() => canAccessSubscriptionFeatures.value && !isFreeTier.value
|
||||
)
|
||||
|
||||
const currentTierKey = computed<TierKey | null>(() =>
|
||||
|
||||
@@ -37,12 +37,12 @@ const emit = defineEmits<{
|
||||
subscribed: []
|
||||
}>()
|
||||
|
||||
const { isActiveSubscription, showSubscriptionDialog, tier } =
|
||||
const { canAccessSubscriptionFeatures, showSubscriptionDialog, tier } =
|
||||
useBillingContext()
|
||||
const isAwaitingStripeSubscription = ref(false)
|
||||
|
||||
watch(
|
||||
[isAwaitingStripeSubscription, isActiveSubscription],
|
||||
[isAwaitingStripeSubscription, canAccessSubscriptionFeatures],
|
||||
([awaiting, isActive]) => {
|
||||
if (isCloud && awaiting && isActive) {
|
||||
emit('subscribed')
|
||||
|
||||
@@ -8,7 +8,7 @@ import { createI18n } from 'vue-i18n'
|
||||
import SubscriptionPanel from '@/platform/cloud/subscription/components/SubscriptionPanel.vue'
|
||||
|
||||
// Mock state refs that can be modified between tests
|
||||
const mockIsActiveSubscription = ref(false)
|
||||
const mockCanAccessSubscriptionFeatures = ref(false)
|
||||
const mockIsCancelled = ref(false)
|
||||
const mockSubscriptionTier = ref<
|
||||
'STANDARD' | 'CREATOR' | 'PRO' | 'FOUNDERS_EDITION' | null
|
||||
@@ -24,7 +24,9 @@ const TIER_TO_NAME: Record<string, string> = {
|
||||
|
||||
// Mock composables - using computed to match composable return types
|
||||
const mockSubscriptionData = {
|
||||
isActiveSubscription: computed(() => mockIsActiveSubscription.value),
|
||||
canAccessSubscriptionFeatures: computed(
|
||||
() => mockCanAccessSubscriptionFeatures.value
|
||||
),
|
||||
isCancelled: computed(() => mockIsCancelled.value),
|
||||
formattedRenewalDate: computed(() => '2024-12-31'),
|
||||
formattedEndDate: computed(() => '2024-12-31'),
|
||||
@@ -93,7 +95,9 @@ vi.mock('@/composables/auth/useAuthActions', () => ({
|
||||
|
||||
vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
useBillingContext: () => ({
|
||||
isActiveSubscription: computed(() => mockIsActiveSubscription.value),
|
||||
canAccessSubscriptionFeatures: computed(
|
||||
() => mockCanAccessSubscriptionFeatures.value
|
||||
),
|
||||
manageSubscription: vi.fn()
|
||||
})
|
||||
}))
|
||||
@@ -240,7 +244,7 @@ describe('SubscriptionPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Reset mock state
|
||||
mockIsActiveSubscription.value = false
|
||||
mockCanAccessSubscriptionFeatures.value = false
|
||||
mockIsCancelled.value = false
|
||||
mockSubscriptionTier.value = 'CREATOR'
|
||||
mockIsYearlySubscription.value = false
|
||||
@@ -250,13 +254,13 @@ describe('SubscriptionPanel', () => {
|
||||
|
||||
describe('subscription state functionality', () => {
|
||||
it('shows correct UI for active subscription', () => {
|
||||
mockIsActiveSubscription.value = true
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
const { container } = createComponent()
|
||||
expect(container.textContent).toContain('Manage Subscription')
|
||||
})
|
||||
|
||||
it('shows correct UI for inactive subscription', () => {
|
||||
mockIsActiveSubscription.value = false
|
||||
mockCanAccessSubscriptionFeatures.value = false
|
||||
const { container } = createComponent()
|
||||
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
|
||||
expect(container.querySelector('subscribe-button-stub')).not.toBeNull()
|
||||
@@ -264,14 +268,14 @@ describe('SubscriptionPanel', () => {
|
||||
})
|
||||
|
||||
it('shows renewal date for active non-cancelled subscription', () => {
|
||||
mockIsActiveSubscription.value = true
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
mockIsCancelled.value = false
|
||||
const { container } = createComponent()
|
||||
expect(container.textContent).toContain('Renews 2024-12-31')
|
||||
})
|
||||
|
||||
it('shows expiry date for cancelled subscription', () => {
|
||||
mockIsActiveSubscription.value = true
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
mockIsCancelled.value = true
|
||||
const { container } = createComponent()
|
||||
expect(container.textContent).toContain('Expires 2024-12-31')
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-inter text-2xl/tight font-semibold">
|
||||
{{
|
||||
isActiveSubscription
|
||||
canAccessSubscriptionFeatures
|
||||
? $t('subscription.title')
|
||||
: $t('subscription.titleUnsubscribed')
|
||||
}}
|
||||
@@ -48,5 +48,5 @@ const teamWorkspacesEnabled = computed(
|
||||
() => isCloud && flags.teamWorkspacesEnabled
|
||||
)
|
||||
|
||||
const { isActiveSubscription } = useBillingContext()
|
||||
const { canAccessSubscriptionFeatures } = useBillingContext()
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<span class="text-base">{{ $t('subscription.perMonth') }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isActiveSubscription"
|
||||
v-if="canAccessSubscriptionFeatures"
|
||||
class="text-sm text-text-secondary"
|
||||
>
|
||||
<template v-if="isCancelled">
|
||||
@@ -33,7 +33,7 @@
|
||||
</div>
|
||||
|
||||
<Button
|
||||
v-if="isActiveSubscription && !isFreeTier"
|
||||
v-if="canAccessSubscriptionFeatures && !isFreeTier"
|
||||
variant="secondary"
|
||||
class="ml-auto rounded-lg bg-interface-menu-component-surface-selected px-4 py-2 text-sm font-normal text-text-primary"
|
||||
@click="
|
||||
@@ -45,7 +45,7 @@
|
||||
{{ $t('subscription.manageSubscription') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="isActiveSubscription"
|
||||
v-if="canAccessSubscriptionFeatures"
|
||||
variant="primary"
|
||||
class="rounded-lg px-4 py-2 text-sm font-normal text-text-primary"
|
||||
@click="showSubscriptionDialog"
|
||||
@@ -54,7 +54,7 @@
|
||||
</Button>
|
||||
|
||||
<SubscribeButton
|
||||
v-if="!isActiveSubscription"
|
||||
v-if="!canAccessSubscriptionFeatures"
|
||||
:label="$t('subscription.subscribeNow')"
|
||||
size="sm"
|
||||
:fluid="false"
|
||||
@@ -137,7 +137,7 @@ const authActions = useAuthActions()
|
||||
const { t, n } = useI18n()
|
||||
|
||||
const {
|
||||
isActiveSubscription,
|
||||
canAccessSubscriptionFeatures,
|
||||
isCancelled,
|
||||
isFreeTier,
|
||||
formattedRenewalDate,
|
||||
|
||||
@@ -169,7 +169,7 @@ const emit = defineEmits<{
|
||||
close: [subscribed: boolean]
|
||||
}>()
|
||||
|
||||
const { isActiveSubscription } = useBillingContext()
|
||||
const { canAccessSubscriptionFeatures } = useBillingContext()
|
||||
|
||||
const isSubscriptionEnabled = (): boolean =>
|
||||
Boolean(isCloud && window.__CONFIG__?.subscription_required)
|
||||
@@ -191,7 +191,7 @@ const telemetry = useTelemetry()
|
||||
const showCustomPricingTable = computed(() => isSubscriptionEnabled())
|
||||
|
||||
watch(
|
||||
() => isActiveSubscription.value,
|
||||
() => canAccessSubscriptionFeatures.value,
|
||||
(isActive) => {
|
||||
if (isActive && showCustomPricingTable.value) {
|
||||
emit('close', true)
|
||||
|
||||
@@ -195,7 +195,7 @@ describe('useSubscription', () => {
|
||||
})
|
||||
|
||||
describe('computed properties', () => {
|
||||
it('should compute isActiveSubscription correctly when subscription is active', async () => {
|
||||
it('should compute canAccessSubscriptionFeatures correctly when subscription is active', async () => {
|
||||
vi.mocked(global.fetch).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
@@ -206,13 +206,14 @@ describe('useSubscription', () => {
|
||||
} as Response)
|
||||
|
||||
mockIsLoggedIn.value = true
|
||||
const { isActiveSubscription, fetchStatus } = useSubscriptionWithScope()
|
||||
const { canAccessSubscriptionFeatures, fetchStatus } =
|
||||
useSubscriptionWithScope()
|
||||
|
||||
await fetchStatus()
|
||||
expect(isActiveSubscription.value).toBe(true)
|
||||
expect(canAccessSubscriptionFeatures.value).toBe(true)
|
||||
})
|
||||
|
||||
it('should compute isActiveSubscription as false when subscription is inactive', async () => {
|
||||
it('should compute canAccessSubscriptionFeatures as false when subscription is inactive', async () => {
|
||||
vi.mocked(global.fetch).mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
@@ -223,10 +224,11 @@ describe('useSubscription', () => {
|
||||
} as Response)
|
||||
|
||||
mockIsLoggedIn.value = true
|
||||
const { isActiveSubscription, fetchStatus } = useSubscriptionWithScope()
|
||||
const { canAccessSubscriptionFeatures, fetchStatus } =
|
||||
useSubscriptionWithScope()
|
||||
|
||||
await fetchStatus()
|
||||
expect(isActiveSubscription.value).toBe(false)
|
||||
expect(canAccessSubscriptionFeatures.value).toBe(false)
|
||||
})
|
||||
|
||||
it('should format renewal date correctly', async () => {
|
||||
@@ -604,12 +606,12 @@ describe('useSubscription', () => {
|
||||
expect(global.fetch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should report isActiveSubscription as true when not on cloud', () => {
|
||||
it('should report canAccessSubscriptionFeatures as true when not on cloud', () => {
|
||||
mockIsCloud.value = false
|
||||
|
||||
const { isActiveSubscription } = useSubscriptionWithScope()
|
||||
const { canAccessSubscriptionFeatures } = useSubscriptionWithScope()
|
||||
|
||||
expect(isActiveSubscription.value).toBe(true)
|
||||
expect(canAccessSubscriptionFeatures.value).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ function useSubscriptionInternal() {
|
||||
const telemetry = useTelemetry()
|
||||
const isInitialized = ref(false)
|
||||
|
||||
const isSubscribedOrIsNotCloud = computed(() => {
|
||||
const canAccessSubscriptionFeatures = computed(() => {
|
||||
if (!isCloud || !window.__CONFIG__?.subscription_required) return true
|
||||
|
||||
return subscriptionStatus.value?.is_active ?? false
|
||||
@@ -225,7 +225,7 @@ function useSubscriptionInternal() {
|
||||
recordPendingSubscriptionCheckoutAttempt({
|
||||
tier: 'standard',
|
||||
cycle: 'monthly',
|
||||
checkout_type: isSubscribedOrIsNotCloud.value ? 'change' : 'new',
|
||||
checkout_type: canAccessSubscriptionFeatures.value ? 'change' : 'new',
|
||||
...(subscriptionTier.value
|
||||
? { previous_tier: TIER_TO_KEY[subscriptionTier.value] }
|
||||
: {}),
|
||||
@@ -258,7 +258,7 @@ function useSubscriptionInternal() {
|
||||
const { startCancellationWatcher, stopCancellationWatcher } =
|
||||
useSubscriptionCancellationWatcher({
|
||||
fetchStatus,
|
||||
isActiveSubscription: isSubscribedOrIsNotCloud,
|
||||
canAccessSubscriptionFeatures: canAccessSubscriptionFeatures,
|
||||
subscriptionStatus,
|
||||
telemetry,
|
||||
shouldWatchCancellation: isSubscriptionEnabled
|
||||
@@ -276,7 +276,7 @@ function useSubscriptionInternal() {
|
||||
const requireActiveSubscription = async (): Promise<void> => {
|
||||
await fetchSubscriptionStatus()
|
||||
|
||||
if (!isSubscribedOrIsNotCloud.value) {
|
||||
if (!canAccessSubscriptionFeatures.value) {
|
||||
showSubscriptionDialog()
|
||||
}
|
||||
}
|
||||
@@ -442,7 +442,7 @@ function useSubscriptionInternal() {
|
||||
|
||||
return {
|
||||
// State
|
||||
isActiveSubscription: isSubscribedOrIsNotCloud,
|
||||
canAccessSubscriptionFeatures: canAccessSubscriptionFeatures,
|
||||
isInitialized,
|
||||
isCancelled,
|
||||
formattedRenewalDate,
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('useSubscriptionCancellationWatcher', () => {
|
||||
baseStatus
|
||||
)
|
||||
const isActive = ref(true)
|
||||
const isActiveSubscription = computed(() => isActive.value)
|
||||
const canAccessSubscriptionFeatures = computed(() => isActive.value)
|
||||
|
||||
let shouldWatch = true
|
||||
const shouldWatchCancellation = () => shouldWatch
|
||||
@@ -77,7 +77,7 @@ describe('useSubscriptionCancellationWatcher', () => {
|
||||
|
||||
const { startCancellationWatcher } = initWatcher({
|
||||
fetchStatus,
|
||||
isActiveSubscription,
|
||||
canAccessSubscriptionFeatures,
|
||||
subscriptionStatus,
|
||||
telemetry: telemetryMock,
|
||||
shouldWatchCancellation
|
||||
@@ -107,7 +107,7 @@ describe('useSubscriptionCancellationWatcher', () => {
|
||||
|
||||
const { startCancellationWatcher } = initWatcher({
|
||||
fetchStatus,
|
||||
isActiveSubscription,
|
||||
canAccessSubscriptionFeatures,
|
||||
subscriptionStatus,
|
||||
telemetry: telemetryMock,
|
||||
shouldWatchCancellation
|
||||
@@ -129,7 +129,7 @@ describe('useSubscriptionCancellationWatcher', () => {
|
||||
|
||||
const { startCancellationWatcher } = initWatcher({
|
||||
fetchStatus,
|
||||
isActiveSubscription,
|
||||
canAccessSubscriptionFeatures,
|
||||
subscriptionStatus,
|
||||
telemetry: telemetryMock,
|
||||
shouldWatchCancellation
|
||||
@@ -154,7 +154,7 @@ describe('useSubscriptionCancellationWatcher', () => {
|
||||
|
||||
const { startCancellationWatcher } = initWatcher({
|
||||
fetchStatus,
|
||||
isActiveSubscription,
|
||||
canAccessSubscriptionFeatures,
|
||||
subscriptionStatus,
|
||||
telemetry: telemetryMock,
|
||||
shouldWatchCancellation
|
||||
|
||||
@@ -12,7 +12,7 @@ const CANCELLATION_BACKOFF_MULTIPLIER = 3 // 5s, 15s, 45s, 135s intervals
|
||||
|
||||
type CancellationWatcherOptions = {
|
||||
fetchStatus: () => Promise<CloudSubscriptionStatusResponse | null | void>
|
||||
isActiveSubscription: ComputedRef<boolean>
|
||||
canAccessSubscriptionFeatures: ComputedRef<boolean>
|
||||
subscriptionStatus: Ref<CloudSubscriptionStatusResponse | null>
|
||||
telemetry: Pick<
|
||||
TelemetryDispatcher,
|
||||
@@ -23,7 +23,7 @@ type CancellationWatcherOptions = {
|
||||
|
||||
export function useSubscriptionCancellationWatcher({
|
||||
fetchStatus,
|
||||
isActiveSubscription,
|
||||
canAccessSubscriptionFeatures,
|
||||
subscriptionStatus,
|
||||
telemetry,
|
||||
shouldWatchCancellation
|
||||
@@ -76,7 +76,7 @@ export function useSubscriptionCancellationWatcher({
|
||||
try {
|
||||
await fetchStatus()
|
||||
|
||||
if (!isActiveSubscription.value) {
|
||||
if (!canAccessSubscriptionFeatures.value) {
|
||||
if (!cancellationTracked.value) {
|
||||
cancellationTracked.value = true
|
||||
try {
|
||||
|
||||
@@ -20,7 +20,7 @@ vi.mock('@/composables/auth/useCurrentUser', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
useBillingContext: () => ({ isActiveSubscription: ref(false) })
|
||||
useBillingContext: () => ({ canAccessSubscriptionFeatures: ref(false) })
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useFeatureFlags', () => ({
|
||||
|
||||
@@ -53,7 +53,7 @@ export function useSettingUI(
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
const { shouldRenderVueNodes } = useVueFeatureFlags()
|
||||
const { isActiveSubscription } = useBillingContext()
|
||||
const { canAccessSubscriptionFeatures } = useBillingContext()
|
||||
|
||||
const teamWorkspacesEnabled = computed(
|
||||
() => isCloud && flags.teamWorkspacesEnabled
|
||||
@@ -154,7 +154,7 @@ export function useSettingUI(
|
||||
|
||||
const shouldShowPlanCreditsPanel = computed(() => {
|
||||
if (!subscriptionPanel) return false
|
||||
return isActiveSubscription.value
|
||||
return canAccessSubscriptionFeatures.value
|
||||
})
|
||||
|
||||
const userPanel: SettingPanelItem = {
|
||||
|
||||
@@ -38,7 +38,7 @@ vi.mock('@/platform/telemetry', () => ({
|
||||
|
||||
vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
useBillingContext: () => ({
|
||||
isActiveSubscription: { value: true },
|
||||
canAccessSubscriptionFeatures: { value: true },
|
||||
isFreeTier: { value: false },
|
||||
type: { value: 'legacy' }
|
||||
})
|
||||
|
||||
@@ -1,161 +1,204 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { computed, defineComponent, ref } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json'
|
||||
import enMessages from '@/locales/en/main.json' with { type: 'json' }
|
||||
|
||||
import CurrentUserPopoverWorkspace from './CurrentUserPopoverWorkspace.vue'
|
||||
|
||||
const showCreateWorkspaceDialog = vi.fn()
|
||||
|
||||
vi.mock('@/composables/auth/useCurrentUser', () => ({
|
||||
useCurrentUser: () => ({
|
||||
userDisplayName: ref('Liz'),
|
||||
userEmail: ref('liz@example.com'),
|
||||
userPhotoUrl: ref(null),
|
||||
handleSignOut: vi.fn()
|
||||
})
|
||||
}))
|
||||
// Mock pinia - preserve actual exports to avoid missing defineStore
|
||||
vi.mock('pinia', async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...(actual as object),
|
||||
storeToRefs: vi.fn((store) => store)
|
||||
}
|
||||
})
|
||||
|
||||
const mockCanAccessSubscriptionFeatures = ref(true)
|
||||
const mockIsFreeTier = ref(false)
|
||||
const mockSubscription = ref({ isCancelled: false })
|
||||
const mockBalance = ref({ effectiveBalanceMicros: 100000 })
|
||||
vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
useBillingContext: () => ({
|
||||
isActiveSubscription: ref(true),
|
||||
isFreeTier: ref(false),
|
||||
subscription: ref(null),
|
||||
balance: ref(null),
|
||||
canAccessSubscriptionFeatures: mockCanAccessSubscriptionFeatures,
|
||||
isFreeTier: mockIsFreeTier,
|
||||
subscription: mockSubscription,
|
||||
balance: mockBalance,
|
||||
isLoading: ref(false),
|
||||
fetchBalance: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/workspace/stores/teamWorkspaceStore', () => ({
|
||||
useTeamWorkspaceStore: () => ({
|
||||
initState: ref('ready'),
|
||||
workspaceName: ref('Test Workspace'),
|
||||
isInPersonalWorkspace: ref(false)
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/workspace/composables/useWorkspaceUI', () => ({
|
||||
useWorkspaceUI: () => ({
|
||||
permissions: computed(() => ({
|
||||
canTopUp: false,
|
||||
canManageSubscription: false
|
||||
}))
|
||||
permissions: ref({
|
||||
canTopUp: true,
|
||||
canManageSubscription: true
|
||||
})
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/auth/useCurrentUser', () => ({
|
||||
useCurrentUser: () => ({
|
||||
userDisplayName: 'Test User',
|
||||
userEmail: 'test@example.com',
|
||||
userPhotoUrl: null,
|
||||
handleSignOut: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/composables/useSettingsDialog', () => ({
|
||||
useSettingsDialog: () => ({
|
||||
show: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/services/dialogService', () => ({
|
||||
useDialogService: () => ({
|
||||
showTopUpCreditsDialog: vi.fn(),
|
||||
showCreateWorkspaceDialog: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock(
|
||||
'@/platform/cloud/subscription/composables/useSubscriptionDialog',
|
||||
() => ({
|
||||
useSubscriptionDialog: () => ({ showPricingTable: vi.fn() })
|
||||
useSubscriptionDialog: () => ({
|
||||
showPricingTable: vi.fn()
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
vi.mock('@/platform/settings/composables/useSettingsDialog', () => ({
|
||||
useSettingsDialog: () => ({ show: vi.fn() })
|
||||
}))
|
||||
|
||||
vi.mock('@/services/dialogService', () => ({
|
||||
useDialogService: () => ({
|
||||
showCreateWorkspaceDialog,
|
||||
showTopUpCreditsDialog: vi.fn()
|
||||
vi.mock('@/composables/useExternalLink', () => ({
|
||||
useExternalLink: () => ({
|
||||
buildDocsUrl: vi.fn().mockReturnValue('https://docs.example.com'),
|
||||
docsPaths: { partnerNodesPricing: '/pricing' }
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/telemetry', () => ({
|
||||
useTelemetry: () => undefined
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useExternalLink', () => ({
|
||||
useExternalLink: () => ({
|
||||
buildDocsUrl: vi.fn(() => 'https://docs.comfy.org'),
|
||||
docsPaths: { partnerNodesPricing: 'partner-nodes' }
|
||||
useTelemetry: () => ({
|
||||
trackAddApiCreditButtonClicked: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
const WorkspaceSwitcherPopoverStub = defineComponent({
|
||||
emits: ['select', 'create'],
|
||||
template: `
|
||||
<div>
|
||||
<button data-testid="stub-select-workspace" @click="$emit('select')" />
|
||||
<button data-testid="stub-create-workspace" @click="$emit('create')" />
|
||||
</div>
|
||||
`
|
||||
})
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
isCloud: true
|
||||
}))
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: { en: enMessages }
|
||||
})
|
||||
vi.mock('@/base/credits/comfyCredits', () => ({
|
||||
formatCreditsFromCents: vi.fn().mockReturnValue('$10.00')
|
||||
}))
|
||||
|
||||
function renderComponent() {
|
||||
return render(CurrentUserPopoverWorkspace, {
|
||||
global: {
|
||||
plugins: [
|
||||
createTestingPinia({
|
||||
createSpy: vi.fn,
|
||||
initialState: {
|
||||
teamWorkspace: {
|
||||
initState: 'ready',
|
||||
activeWorkspaceId: 'ws-personal'
|
||||
}
|
||||
}
|
||||
}),
|
||||
i18n
|
||||
],
|
||||
directives: {
|
||||
tooltip: {}
|
||||
},
|
||||
stubs: {
|
||||
WorkspaceSwitcherPopover: WorkspaceSwitcherPopoverStub,
|
||||
SubscribeButton: true,
|
||||
UserAvatar: true,
|
||||
WorkspaceProfilePic: true,
|
||||
Skeleton: true,
|
||||
Divider: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
// Mock child components
|
||||
vi.mock('@/components/common/UserAvatar.vue', () => ({
|
||||
default: { template: '<div data-testid="user-avatar"></div>' }
|
||||
}))
|
||||
|
||||
vi.mock('./WorkspaceProfilePic.vue', () => ({
|
||||
default: { template: '<div data-testid="workspace-pic"></div>' }
|
||||
}))
|
||||
|
||||
vi.mock('./WorkspaceSwitcherPopover.vue', () => ({
|
||||
default: { template: '<div data-testid="workspace-switcher"></div>' }
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/button/Button.vue', () => ({
|
||||
default: {
|
||||
template: '<button :data-testid="$attrs[\'data-testid\']"><slot /></button>'
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/cloud/subscription/components/SubscribeButton.vue', () => ({
|
||||
default: {
|
||||
template: '<button data-testid="subscribe-button">Subscribe</button>'
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('primevue/divider', () => ({
|
||||
default: { template: '<hr />' }
|
||||
}))
|
||||
|
||||
vi.mock('primevue/popover', () => ({
|
||||
default: { template: '<div><slot /></div>' }
|
||||
}))
|
||||
|
||||
vi.mock('primevue/skeleton', () => ({
|
||||
default: { template: '<div data-testid="skeleton"></div>' }
|
||||
}))
|
||||
|
||||
describe('CurrentUserPopoverWorkspace', () => {
|
||||
it('toggles the workspace switcher panel from the selector row', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('workspace-switcher-panel')
|
||||
).not.toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByTestId('workspace-switcher-trigger'))
|
||||
expect(screen.getByTestId('workspace-switcher-panel')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByTestId('workspace-switcher-trigger'))
|
||||
expect(
|
||||
screen.queryByTestId('workspace-switcher-panel')
|
||||
).not.toBeInTheDocument()
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
mockIsFreeTier.value = false
|
||||
mockSubscription.value = { isCancelled: false }
|
||||
})
|
||||
|
||||
it('closes the switcher panel after selecting a workspace', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderComponent()
|
||||
function renderComponent() {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: { en: enMessages }
|
||||
})
|
||||
|
||||
await user.click(screen.getByTestId('workspace-switcher-trigger'))
|
||||
await user.click(screen.getByTestId('stub-select-workspace'))
|
||||
return render(CurrentUserPopoverWorkspace, {
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
directives: {
|
||||
tooltip: {}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('workspace-switcher-panel')
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
describe('canAccessSubscriptionFeatures', () => {
|
||||
it('shows add credits button when canAccessSubscriptionFeatures is true and not free tier', () => {
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
mockIsFreeTier.value = false
|
||||
renderComponent()
|
||||
|
||||
it('opens the create-workspace dialog and closes the popover on create', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { emitted } = renderComponent()
|
||||
expect(screen.getByTestId('add-credits-button')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByTestId('upgrade-to-add-credits-button')
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
await user.click(screen.getByTestId('workspace-switcher-trigger'))
|
||||
await user.click(screen.getByTestId('stub-create-workspace'))
|
||||
it('shows upgrade button when canAccessSubscriptionFeatures is true and is free tier', () => {
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
mockIsFreeTier.value = true
|
||||
renderComponent()
|
||||
|
||||
expect(showCreateWorkspaceDialog).toHaveBeenCalled()
|
||||
expect(emitted('close')).toHaveLength(1)
|
||||
expect(
|
||||
screen.queryByTestId('workspace-switcher-panel')
|
||||
).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByTestId('upgrade-to-add-credits-button')
|
||||
).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('add-credits-button')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows manage plan when canAccessSubscriptionFeatures is true', () => {
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
renderComponent()
|
||||
|
||||
expect(screen.getByTestId('manage-plan-menu-item')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides manage plan when canAccessSubscriptionFeatures is false', () => {
|
||||
mockCanAccessSubscriptionFeatures.value = false
|
||||
renderComponent()
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('manage-plan-menu-item')
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -81,7 +81,9 @@
|
||||
</Button>
|
||||
<!-- Upgrade to add credits (free tier) -->
|
||||
<Button
|
||||
v-if="isActiveSubscription && permissions.canTopUp && isFreeTier"
|
||||
v-if="
|
||||
canAccessSubscriptionFeatures && permissions.canTopUp && isFreeTier
|
||||
"
|
||||
variant="gradient"
|
||||
size="sm"
|
||||
data-testid="upgrade-to-add-credits-button"
|
||||
@@ -91,7 +93,7 @@
|
||||
</Button>
|
||||
<!-- Add Credits (subscribed + personal or workspace owner only, paid tier) -->
|
||||
<Button
|
||||
v-else-if="isActiveSubscription && permissions.canTopUp"
|
||||
v-else-if="canAccessSubscriptionFeatures && permissions.canTopUp"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
class="text-base-foreground"
|
||||
@@ -268,7 +270,7 @@ const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
|
||||
const settingsDialog = useSettingsDialog()
|
||||
const dialogService = useDialogService()
|
||||
const {
|
||||
isActiveSubscription,
|
||||
canAccessSubscriptionFeatures,
|
||||
isFreeTier,
|
||||
subscription,
|
||||
balance,
|
||||
@@ -302,12 +304,14 @@ const showPlansAndPricing = computed(
|
||||
() => permissions.value.canManageSubscription
|
||||
)
|
||||
const showManagePlan = computed(
|
||||
() => permissions.value.canManageSubscription && isActiveSubscription.value
|
||||
() =>
|
||||
permissions.value.canManageSubscription &&
|
||||
canAccessSubscriptionFeatures.value
|
||||
)
|
||||
const showSubscribeAction = computed(
|
||||
() =>
|
||||
permissions.value.canManageSubscription &&
|
||||
(!isActiveSubscription.value || isCancelled.value)
|
||||
(!canAccessSubscriptionFeatures.value || isCancelled.value)
|
||||
)
|
||||
|
||||
const handleOpenUserSettings = () => {
|
||||
|
||||
@@ -126,7 +126,9 @@ const mockError = ref<string | null>(null)
|
||||
|
||||
vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
useBillingContext: () => ({
|
||||
isActiveSubscription: computed(() => mockIsActiveSubscription.value),
|
||||
canAccessSubscriptionFeatures: computed(
|
||||
() => mockIsActiveSubscription.value
|
||||
),
|
||||
isFreeTier: computed(() => false),
|
||||
subscription: mockSubscription,
|
||||
teamCreditStops: mockTeamCreditStops,
|
||||
@@ -180,7 +182,9 @@ vi.mock('@/platform/workspace/composables/useWorkspaceUI', () => ({
|
||||
})),
|
||||
uiConfig: computed(() => mockUiConfig.value),
|
||||
isInPersonalWorkspace: mockIsInPersonalWorkspace,
|
||||
isActiveSubscription: computed(() => mockIsActiveSubscription.value),
|
||||
canAccessSubscriptionFeatures: computed(
|
||||
() => mockIsActiveSubscription.value
|
||||
),
|
||||
isOriginalOwner: mockIsOriginalOwner,
|
||||
isTeamPlanCancelled: mockIsTeamPlanCancelled,
|
||||
isDeleteDisabled: mockIsDeleteDisabled,
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
<span class="text-base">{{ priceUnitLabel }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isActiveSubscription"
|
||||
v-if="canAccessSubscriptionFeatures"
|
||||
class="text-sm text-text-secondary"
|
||||
>
|
||||
<template v-if="isTeamPlanCancelled">
|
||||
@@ -182,7 +182,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isActiveSubscription"
|
||||
v-if="canAccessSubscriptionFeatures"
|
||||
class="flex flex-wrap gap-2 md:ml-auto"
|
||||
>
|
||||
<Button
|
||||
@@ -246,7 +246,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isActiveSubscription || isPersonalFree"
|
||||
v-if="canAccessSubscriptionFeatures || isPersonalFree"
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<i18n-t
|
||||
@@ -342,7 +342,7 @@ const billingOperationStore = useBillingOperationStore()
|
||||
const isSettingUp = computed(() => billingOperationStore.isSettingUp)
|
||||
|
||||
const {
|
||||
isActiveSubscription,
|
||||
canAccessSubscriptionFeatures,
|
||||
isFreeTier: isFreeTierPlan,
|
||||
subscription,
|
||||
isLoading,
|
||||
@@ -363,7 +363,7 @@ const { menuEntries } = useWorkspaceMenuItems()
|
||||
const showSubscribePrompt = computed(() => {
|
||||
if (!permissions.value.canManageSubscription) return false
|
||||
if (isTeamPlanCancelled.value) return false
|
||||
if (isInPersonalWorkspace.value) return !isActiveSubscription.value
|
||||
if (isInPersonalWorkspace.value) return !canAccessSubscriptionFeatures.value
|
||||
return !isWorkspaceSubscribed.value
|
||||
})
|
||||
|
||||
@@ -376,13 +376,13 @@ const isPersonalFree = computed(
|
||||
)
|
||||
|
||||
const isTeamActive = computed(
|
||||
() => !isInPersonalWorkspace.value && isActiveSubscription.value
|
||||
() => !isInPersonalWorkspace.value && canAccessSubscriptionFeatures.value
|
||||
)
|
||||
|
||||
const isMemberView = computed(
|
||||
() =>
|
||||
!permissions.value.canManageSubscription &&
|
||||
!isActiveSubscription.value &&
|
||||
!canAccessSubscriptionFeatures.value &&
|
||||
!isWorkspaceSubscribed.value
|
||||
)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
>
|
||||
<h2 class="m-0 text-sm font-normal text-base-foreground">
|
||||
{{
|
||||
isActiveSubscription
|
||||
canAccessSubscriptionFeatures
|
||||
? $t('workspacePanel.inviteUpsellDialog.titleSingleSeat')
|
||||
: $t('workspacePanel.inviteUpsellDialog.titleNotSubscribed')
|
||||
}}
|
||||
@@ -26,7 +26,7 @@
|
||||
<div class="flex flex-col gap-4 p-4">
|
||||
<p class="m-0 text-sm text-muted-foreground">
|
||||
{{
|
||||
isActiveSubscription
|
||||
canAccessSubscriptionFeatures
|
||||
? $t('workspacePanel.inviteUpsellDialog.messageSingleSeat')
|
||||
: $t('workspacePanel.inviteUpsellDialog.messageNotSubscribed')
|
||||
}}
|
||||
@@ -52,7 +52,7 @@ import { useSubscriptionDialog } from '@/platform/cloud/subscription/composables
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const dialogStore = useDialogStore()
|
||||
const { isActiveSubscription } = useBillingContext()
|
||||
const { canAccessSubscriptionFeatures } = useBillingContext()
|
||||
const subscriptionDialog = useSubscriptionDialog()
|
||||
|
||||
function onDismiss() {
|
||||
|
||||
@@ -265,7 +265,7 @@ const {
|
||||
mockIsInviteLimitReached,
|
||||
mockPermissions,
|
||||
mockUiConfig,
|
||||
mockIsActiveSubscription,
|
||||
mockCanAccessSubscriptionFeatures,
|
||||
mockSubscription
|
||||
} = vi.hoisted(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/consistent-type-imports
|
||||
@@ -302,7 +302,7 @@ const {
|
||||
workspaceMenuAction: 'delete' as 'leave' | 'delete' | null,
|
||||
workspaceMenuDisabledTooltip: null as string | null
|
||||
}),
|
||||
mockIsActiveSubscription: ref(true),
|
||||
mockCanAccessSubscriptionFeatures: ref(true),
|
||||
mockSubscription: ref<{ tier: string; isCancelled?: boolean } | null>({
|
||||
tier: 'PRO',
|
||||
isCancelled: false
|
||||
@@ -364,7 +364,7 @@ vi.mock(
|
||||
|
||||
vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
useBillingContext: () => ({
|
||||
isActiveSubscription: mockIsActiveSubscription,
|
||||
canAccessSubscriptionFeatures: mockCanAccessSubscriptionFeatures,
|
||||
subscription: mockSubscription,
|
||||
getMaxSeats: (tierKey: string) => {
|
||||
const seats: Record<string, number> = {
|
||||
@@ -405,7 +405,7 @@ describe('useMembersPanel', () => {
|
||||
mockIsWorkspaceSubscribed.value = true
|
||||
mockTotalMemberSlots.value = 0
|
||||
mockIsInviteLimitReached.value = false
|
||||
mockIsActiveSubscription.value = true
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
mockSubscription.value = { tier: 'PRO', isCancelled: false }
|
||||
})
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ describe('useWorkspaceBilling', () => {
|
||||
it('exposes a null subscription before any status fetch', () => {
|
||||
const billing = setupBilling()
|
||||
expect(billing.subscription.value).toBeNull()
|
||||
expect(billing.isActiveSubscription.value).toBe(false)
|
||||
expect(billing.canAccessSubscriptionFeatures.value).toBe(false)
|
||||
expect(billing.isFreeTier.value).toBe(false)
|
||||
})
|
||||
|
||||
@@ -191,7 +191,7 @@ describe('useWorkspaceBilling', () => {
|
||||
isCancelled: true,
|
||||
hasFunds: true
|
||||
})
|
||||
expect(billing.isActiveSubscription.value).toBe(true)
|
||||
expect(billing.canAccessSubscriptionFeatures.value).toBe(true)
|
||||
expect(billing.isFreeTier.value).toBe(false)
|
||||
})
|
||||
|
||||
@@ -205,7 +205,7 @@ describe('useWorkspaceBilling', () => {
|
||||
await billing.fetchStatus()
|
||||
|
||||
expect(billing.subscription.value?.isCancelled).toBe(false)
|
||||
expect(billing.isActiveSubscription.value).toBe(true)
|
||||
expect(billing.canAccessSubscriptionFeatures.value).toBe(true)
|
||||
})
|
||||
|
||||
it('reports free tier when status tier is FREE', async () => {
|
||||
|
||||
@@ -37,7 +37,7 @@ export function useWorkspaceBilling(): BillingState & BillingActions {
|
||||
const statusData = shallowRef<BillingStatusResponse | null>(null)
|
||||
const balanceData = shallowRef<BillingBalanceResponse | null>(null)
|
||||
|
||||
const isActiveSubscription = computed(
|
||||
const canAccessSubscriptionFeatures = computed(
|
||||
() => statusData.value?.is_active ?? false
|
||||
)
|
||||
const isFreeTier = computed(
|
||||
@@ -274,7 +274,7 @@ export function useWorkspaceBilling(): BillingState & BillingActions {
|
||||
|
||||
async function requireActiveSubscription(): Promise<void> {
|
||||
await fetchStatus()
|
||||
if (!isActiveSubscription.value) {
|
||||
if (!canAccessSubscriptionFeatures.value) {
|
||||
subscriptionDialog.show()
|
||||
}
|
||||
}
|
||||
@@ -294,7 +294,7 @@ export function useWorkspaceBilling(): BillingState & BillingActions {
|
||||
currentTeamCreditStop,
|
||||
isLoading,
|
||||
error,
|
||||
isActiveSubscription,
|
||||
canAccessSubscriptionFeatures,
|
||||
isFreeTier,
|
||||
billingStatus,
|
||||
subscriptionStatus,
|
||||
|
||||
@@ -19,7 +19,7 @@ export function useWorkspaceMenuItems() {
|
||||
const {
|
||||
uiConfig,
|
||||
isInPersonalWorkspace,
|
||||
isActiveSubscription,
|
||||
canAccessSubscriptionFeatures,
|
||||
isOriginalOwner,
|
||||
isTeamPlanCancelled,
|
||||
isDeleteDisabled,
|
||||
@@ -51,7 +51,7 @@ export function useWorkspaceMenuItems() {
|
||||
const canCancelPlan = computed(
|
||||
() =>
|
||||
isOriginalOwner.value &&
|
||||
isActiveSubscription.value &&
|
||||
canAccessSubscriptionFeatures.value &&
|
||||
!isTeamPlanCancelled.value &&
|
||||
!isFreeTier.value
|
||||
)
|
||||
@@ -59,7 +59,7 @@ export function useWorkspaceMenuItems() {
|
||||
const canDeleteWorkspace = computed(
|
||||
() =>
|
||||
isOriginalOwner.value &&
|
||||
(!isInPersonalWorkspace.value || isActiveSubscription.value)
|
||||
(!isInPersonalWorkspace.value || canAccessSubscriptionFeatures.value)
|
||||
)
|
||||
|
||||
const canLeaveWorkspace = computed(
|
||||
|
||||
@@ -41,7 +41,7 @@ vi.mock('@/composables/auth/useCurrentUser', () => ({
|
||||
|
||||
vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
useBillingContext: () => ({
|
||||
isActiveSubscription: ref(mockIsActiveSubscription.value),
|
||||
canAccessSubscriptionFeatures: ref(mockIsActiveSubscription.value),
|
||||
subscription: ref({ isCancelled: mockIsCancelled.value })
|
||||
})
|
||||
}))
|
||||
|
||||
@@ -162,7 +162,7 @@ function isOriginalOwnerByEmail(
|
||||
function useWorkspaceUIInternal() {
|
||||
const store = useTeamWorkspaceStore()
|
||||
const { userEmail } = useCurrentUser()
|
||||
const { isActiveSubscription, subscription } = useBillingContext()
|
||||
const { canAccessSubscriptionFeatures, subscription } = useBillingContext()
|
||||
|
||||
const isInPersonalWorkspace = computed(() => store.isInPersonalWorkspace)
|
||||
const isWorkspaceSubscribed = computed(() => store.isWorkspaceSubscribed)
|
||||
@@ -222,7 +222,8 @@ function useWorkspaceUIInternal() {
|
||||
// their menus can't desync on a billing-flag change.
|
||||
const isDeleteDisabled = computed(
|
||||
() =>
|
||||
isActiveSubscription.value && !(subscription.value?.isCancelled ?? false)
|
||||
canAccessSubscriptionFeatures.value &&
|
||||
!(subscription.value?.isCancelled ?? false)
|
||||
)
|
||||
|
||||
const deleteDisabledTooltipKey = computed(() =>
|
||||
@@ -237,7 +238,7 @@ function useWorkspaceUIInternal() {
|
||||
workspaceRole,
|
||||
isInPersonalWorkspace,
|
||||
isWorkspaceSubscribed,
|
||||
isActiveSubscription,
|
||||
canAccessSubscriptionFeatures,
|
||||
isOriginalOwner,
|
||||
isTeamPlanCancelled,
|
||||
isDeleteDisabled,
|
||||
|
||||
147
src/renderer/extensions/linearMode/LinearControls.test.ts
Normal file
147
src/renderer/extensions/linearMode/LinearControls.test.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json' with { type: 'json' }
|
||||
|
||||
import LinearControls from './LinearControls.vue'
|
||||
|
||||
// Mock all dependencies - preserve actual exports to avoid missing defineStore
|
||||
vi.mock('pinia', async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...(actual as object),
|
||||
storeToRefs: vi.fn((store) => store)
|
||||
}
|
||||
})
|
||||
|
||||
const mockCanAccessSubscriptionFeatures = ref(true)
|
||||
vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
useBillingContext: () => ({
|
||||
canAccessSubscriptionFeatures: mockCanAccessSubscriptionFeatures
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/commandStore', () => ({
|
||||
useCommandStore: () => ({
|
||||
execute: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/queueStore', () => ({
|
||||
useQueueSettingsStore: () => ({
|
||||
batchCount: ref(1)
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: () => ({
|
||||
get: vi.fn().mockReturnValue(10)
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
activeWorkflow: { filename: 'test.json' }
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useAppMode', () => ({
|
||||
useAppMode: () => ({
|
||||
isBuilderMode: ref(false)
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/appModeStore', () => ({
|
||||
useAppModeStore: () => ({
|
||||
hasOutputs: ref(true)
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/telemetry', () => ({
|
||||
useTelemetry: () => ({
|
||||
trackUiButtonClicked: vi.fn()
|
||||
})
|
||||
}))
|
||||
|
||||
// Mock child components
|
||||
vi.mock('@/components/builder/AppModeWidgetList.vue', () => ({
|
||||
default: { template: '<div data-testid="widget-list"></div>' }
|
||||
}))
|
||||
|
||||
vi.mock('@/components/loader/Loader.vue', () => ({
|
||||
default: { template: '<div data-testid="loader"></div>' }
|
||||
}))
|
||||
|
||||
vi.mock('@/components/common/ScrubableNumberInput.vue', () => ({
|
||||
default: { template: '<input data-testid="number-input" />' }
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/Popover.vue', () => ({
|
||||
default: { template: '<div><slot name="button" /><slot /></div>' }
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/button/Button.vue', () => ({
|
||||
default: { template: '<button><slot /></button>' }
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/cloud/subscription/components/SubscribeToRun.vue', () => ({
|
||||
default: { template: '<div data-testid="subscribe-to-run">Subscribe</div>' }
|
||||
}))
|
||||
|
||||
vi.mock('./PartnerNodesList.vue', () => ({
|
||||
default: { template: '<div data-testid="partner-nodes"></div>' }
|
||||
}))
|
||||
|
||||
describe('LinearControls', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
})
|
||||
|
||||
function renderComponent(props: { mobile?: boolean } = {}) {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: { en: enMessages }
|
||||
})
|
||||
|
||||
return render(LinearControls, {
|
||||
global: {
|
||||
plugins: [i18n]
|
||||
},
|
||||
props
|
||||
})
|
||||
}
|
||||
|
||||
describe('canAccessSubscriptionFeatures', () => {
|
||||
it('hides SubscribeToRunButton when canAccessSubscriptionFeatures is true', () => {
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
renderComponent()
|
||||
|
||||
expect(screen.queryByTestId('subscribe-to-run')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows SubscribeToRunButton when canAccessSubscriptionFeatures is false', () => {
|
||||
mockCanAccessSubscriptionFeatures.value = false
|
||||
renderComponent()
|
||||
|
||||
expect(screen.getByTestId('subscribe-to-run')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows SubscribeToRunButton on mobile when canAccessSubscriptionFeatures is false', () => {
|
||||
mockCanAccessSubscriptionFeatures.value = false
|
||||
renderComponent({ mobile: true })
|
||||
|
||||
expect(screen.getByTestId('subscribe-to-run')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides SubscribeToRunButton on mobile when canAccessSubscriptionFeatures is true', () => {
|
||||
mockCanAccessSubscriptionFeatures.value = true
|
||||
renderComponent({ mobile: true })
|
||||
|
||||
expect(screen.queryByTestId('subscribe-to-run')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -23,7 +23,7 @@ const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const { batchCount } = storeToRefs(useQueueSettingsStore())
|
||||
const settingStore = useSettingStore()
|
||||
const { isActiveSubscription } = useBillingContext()
|
||||
const { canAccessSubscriptionFeatures } = useBillingContext()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { isBuilderMode } = useAppMode()
|
||||
const appModeStore = useAppModeStore()
|
||||
@@ -138,7 +138,7 @@ function handleDragDrop() {
|
||||
class="border-t border-node-component-border p-4 pb-6"
|
||||
>
|
||||
<SubscribeToRunButton
|
||||
v-if="!isActiveSubscription"
|
||||
v-if="!canAccessSubscriptionFeatures"
|
||||
class="mt-4 w-full"
|
||||
/>
|
||||
<div v-else class="mt-4 flex">
|
||||
@@ -190,7 +190,7 @@ function handleDragDrop() {
|
||||
class="h-7 min-w-40"
|
||||
/>
|
||||
<SubscribeToRunButton
|
||||
v-if="!isActiveSubscription"
|
||||
v-if="!canAccessSubscriptionFeatures"
|
||||
class="mt-4 w-full"
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -29,7 +29,7 @@ vi.mock('@/platform/distribution/types', () => ({
|
||||
|
||||
vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
useBillingContext: () => ({
|
||||
isActiveSubscription: { value: true },
|
||||
canAccessSubscriptionFeatures: { value: true },
|
||||
isFreeTier: { value: false },
|
||||
type: { value: 'legacy' }
|
||||
})
|
||||
|
||||
@@ -25,7 +25,7 @@ vi.mock('@/platform/distribution/types', () => ({
|
||||
|
||||
vi.mock('@/composables/billing/useBillingContext', () => ({
|
||||
useBillingContext: () => ({
|
||||
isActiveSubscription: { value: true },
|
||||
canAccessSubscriptionFeatures: { value: true },
|
||||
isFreeTier: { value: false },
|
||||
type: { value: 'legacy' }
|
||||
})
|
||||
|
||||
@@ -329,8 +329,9 @@ export const useDialogService = () => {
|
||||
async function showTopUpCreditsDialog(options?: {
|
||||
isInsufficientCredits?: boolean
|
||||
}) {
|
||||
const { isActiveSubscription, isFreeTier, type } = useBillingContext()
|
||||
if (!isActiveSubscription.value || isFreeTier.value) {
|
||||
const { canAccessSubscriptionFeatures, isFreeTier, type } =
|
||||
useBillingContext()
|
||||
if (!canAccessSubscriptionFeatures.value || isFreeTier.value) {
|
||||
await showSubscriptionRequiredDialog({
|
||||
reason: options?.isInsufficientCredits
|
||||
? 'out_of_credits'
|
||||
|
||||
@@ -26,7 +26,7 @@ export function useBillingContext(): BillingContext {
|
||||
currentTeamCreditStop: computed(() => null),
|
||||
isLoading: ref(false),
|
||||
error: ref<string | null>(null),
|
||||
isActiveSubscription: computed(() => false),
|
||||
canAccessSubscriptionFeatures: computed(() => false),
|
||||
isFreeTier: computed(() => false),
|
||||
isLegacyTeamPlan: computed(() => false),
|
||||
billingStatus: computed(() => null),
|
||||
|
||||
Reference in New Issue
Block a user