mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
## Summary Differentiates the subscription pricing dialog between personal and team workspaces with distinct visual treatments and a two-stage team workspace upgrade flow. ### Changes - **Personal pricing dialog**: Shows "P" avatar badge, "Plans for Personal Workspace" header, and "Solo use only – Need team workspace?" banner on each tier card - **Team pricing dialog**: Shows workspace avatar, "Plans for Team Workspace" header (emerald), green "Invite up to X members" badge, and emerald border on Creator card - **Two-stage upgrade flow**: "Need team workspace?" → closes pricing → opens CreateWorkspaceDialog → sessionStorage flag → page reload → WorkspaceAuthGate auto-opens team pricing dialog - **Spacing**: Reduced vertical gaps/padding/font sizes so the table fits without scrolling ### Key decisions - sessionStorage key `comfy:resume-team-pricing` bridges the page reload during workspace creation - `onChooseTeam` prop is conditionally passed only to the personal variant - `resumePendingPricingFlow()` is called from WorkspaceAuthGate after workspace initialization ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9901-feat-differentiate-personal-team-pricing-table-with-two-stage-team-workspace-flow-3226d73d365081e7af60dcca86e83673) by [Unito](https://www.unito.io)
229 lines
6.8 KiB
TypeScript
229 lines
6.8 KiB
TypeScript
import { flushPromises, mount } from '@vue/test-utils'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { ref } from 'vue'
|
|
import { createI18n } from 'vue-i18n'
|
|
|
|
import WorkspaceAuthGate from './WorkspaceAuthGate.vue'
|
|
|
|
const mockIsInitialized = ref(false)
|
|
const mockCurrentUser = ref<object | null>(null)
|
|
|
|
vi.mock('@/stores/firebaseAuthStore', () => ({
|
|
useFirebaseAuthStore: () => ({
|
|
isInitialized: mockIsInitialized,
|
|
currentUser: mockCurrentUser
|
|
})
|
|
}))
|
|
|
|
const mockRefreshRemoteConfig = vi.fn()
|
|
vi.mock('@/platform/remoteConfig/refreshRemoteConfig', () => ({
|
|
refreshRemoteConfig: (options: unknown) => mockRefreshRemoteConfig(options)
|
|
}))
|
|
|
|
const mockTeamWorkspacesEnabled = vi.hoisted(() => ({ value: false }))
|
|
vi.mock('@/composables/useFeatureFlags', () => ({
|
|
useFeatureFlags: () => ({
|
|
flags: {
|
|
get teamWorkspacesEnabled() {
|
|
return mockTeamWorkspacesEnabled.value
|
|
}
|
|
}
|
|
})
|
|
}))
|
|
|
|
const mockWorkspaceStoreInitialize = vi.fn()
|
|
const mockWorkspaceStoreInitState = vi.hoisted(() => ({
|
|
value: 'uninitialized' as string
|
|
}))
|
|
vi.mock('@/platform/workspace/stores/teamWorkspaceStore', () => ({
|
|
useTeamWorkspaceStore: () => ({
|
|
get initState() {
|
|
return mockWorkspaceStoreInitState.value
|
|
},
|
|
initialize: mockWorkspaceStoreInitialize
|
|
})
|
|
}))
|
|
|
|
const mockIsCloud = vi.hoisted(() => ({ value: true }))
|
|
vi.mock('@/platform/distribution/types', () => ({
|
|
get isCloud() {
|
|
return mockIsCloud.value
|
|
}
|
|
}))
|
|
|
|
const mockResumePendingPricingFlow = vi.fn()
|
|
vi.mock(
|
|
'@/platform/cloud/subscription/composables/useSubscriptionDialog',
|
|
() => ({
|
|
useSubscriptionDialog: () => ({
|
|
show: vi.fn(),
|
|
showPricingTable: vi.fn(),
|
|
hide: vi.fn(),
|
|
startTeamWorkspaceUpgradeFlow: vi.fn(),
|
|
resumePendingPricingFlow: mockResumePendingPricingFlow
|
|
})
|
|
})
|
|
)
|
|
|
|
describe('WorkspaceAuthGate', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockIsCloud.value = true
|
|
mockIsInitialized.value = false
|
|
mockCurrentUser.value = null
|
|
mockTeamWorkspacesEnabled.value = false
|
|
mockWorkspaceStoreInitState.value = 'uninitialized'
|
|
mockRefreshRemoteConfig.mockResolvedValue(undefined)
|
|
mockWorkspaceStoreInitialize.mockResolvedValue(undefined)
|
|
})
|
|
|
|
const i18n = createI18n({ legacy: false })
|
|
|
|
const mountComponent = () =>
|
|
mount(WorkspaceAuthGate, {
|
|
global: { plugins: [i18n] },
|
|
slots: {
|
|
default: '<div data-testid="slot-content">App Content</div>'
|
|
}
|
|
})
|
|
|
|
describe('non-cloud builds', () => {
|
|
it('renders slot immediately when isCloud is false', async () => {
|
|
mockIsCloud.value = false
|
|
|
|
const wrapper = mountComponent()
|
|
await flushPromises()
|
|
|
|
expect(wrapper.find('[data-testid="slot-content"]').exists()).toBe(true)
|
|
expect(mockRefreshRemoteConfig).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('cloud builds - unauthenticated user', () => {
|
|
it('hides slot while waiting for Firebase auth', () => {
|
|
mockIsInitialized.value = false
|
|
|
|
const wrapper = mountComponent()
|
|
|
|
expect(wrapper.find('[data-testid="slot-content"]').exists()).toBe(false)
|
|
})
|
|
|
|
it('renders slot when Firebase initializes with no user', async () => {
|
|
mockIsInitialized.value = false
|
|
|
|
const wrapper = mountComponent()
|
|
expect(wrapper.find('[data-testid="slot-content"]').exists()).toBe(false)
|
|
|
|
mockIsInitialized.value = true
|
|
mockCurrentUser.value = null
|
|
await flushPromises()
|
|
|
|
expect(wrapper.find('[data-testid="slot-content"]').exists()).toBe(true)
|
|
expect(mockRefreshRemoteConfig).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('cloud builds - authenticated user', () => {
|
|
beforeEach(() => {
|
|
mockIsInitialized.value = true
|
|
mockCurrentUser.value = { uid: 'user-123' }
|
|
})
|
|
|
|
it('refreshes remote config with auth after Firebase init', async () => {
|
|
mountComponent()
|
|
await flushPromises()
|
|
|
|
expect(mockRefreshRemoteConfig).toHaveBeenCalledWith({ useAuth: true })
|
|
})
|
|
|
|
it('renders slot when teamWorkspacesEnabled is false', async () => {
|
|
mockTeamWorkspacesEnabled.value = false
|
|
|
|
const wrapper = mountComponent()
|
|
await flushPromises()
|
|
|
|
expect(wrapper.find('[data-testid="slot-content"]').exists()).toBe(true)
|
|
expect(mockWorkspaceStoreInitialize).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('initializes workspace store when teamWorkspacesEnabled is true', async () => {
|
|
mockTeamWorkspacesEnabled.value = true
|
|
|
|
const wrapper = mountComponent()
|
|
await flushPromises()
|
|
|
|
expect(mockWorkspaceStoreInitialize).toHaveBeenCalled()
|
|
expect(wrapper.find('[data-testid="slot-content"]').exists()).toBe(true)
|
|
})
|
|
|
|
it('calls resumePendingPricingFlow after successful workspace init', async () => {
|
|
mockTeamWorkspacesEnabled.value = true
|
|
mockWorkspaceStoreInitState.value = 'ready'
|
|
|
|
mountComponent()
|
|
await flushPromises()
|
|
|
|
expect(mockResumePendingPricingFlow).toHaveBeenCalled()
|
|
})
|
|
|
|
it('skips workspace init when store is already initialized', async () => {
|
|
mockTeamWorkspacesEnabled.value = true
|
|
mockWorkspaceStoreInitState.value = 'ready'
|
|
|
|
const wrapper = mountComponent()
|
|
await flushPromises()
|
|
|
|
expect(mockWorkspaceStoreInitialize).not.toHaveBeenCalled()
|
|
expect(wrapper.find('[data-testid="slot-content"]').exists()).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('error handling - graceful degradation', () => {
|
|
beforeEach(() => {
|
|
mockIsInitialized.value = true
|
|
mockCurrentUser.value = { uid: 'user-123' }
|
|
})
|
|
|
|
it('renders slot when remote config refresh fails', async () => {
|
|
mockRefreshRemoteConfig.mockRejectedValue(new Error('Network error'))
|
|
|
|
const wrapper = mountComponent()
|
|
await flushPromises()
|
|
|
|
expect(wrapper.find('[data-testid="slot-content"]').exists()).toBe(true)
|
|
})
|
|
|
|
it('renders slot when remote config refresh times out', async () => {
|
|
vi.useFakeTimers()
|
|
// Never-resolving promise simulates a hanging request
|
|
mockRefreshRemoteConfig.mockReturnValue(new Promise(() => {}))
|
|
|
|
const wrapper = mountComponent()
|
|
await flushPromises()
|
|
|
|
// Slot not yet rendered before timeout
|
|
expect(wrapper.find('[data-testid="slot-content"]').exists()).toBe(false)
|
|
|
|
// Advance past the 10 second timeout
|
|
await vi.advanceTimersByTimeAsync(10_001)
|
|
await flushPromises()
|
|
|
|
// Should render slot after timeout (graceful degradation)
|
|
expect(wrapper.find('[data-testid="slot-content"]').exists()).toBe(true)
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it('renders slot when workspace store initialization fails', async () => {
|
|
mockTeamWorkspacesEnabled.value = true
|
|
mockWorkspaceStoreInitialize.mockRejectedValue(
|
|
new Error('Workspace init failed')
|
|
)
|
|
|
|
const wrapper = mountComponent()
|
|
await flushPromises()
|
|
|
|
expect(wrapper.find('[data-testid="slot-content"]').exists()).toBe(true)
|
|
})
|
|
})
|
|
})
|