mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-03 20:51:58 +00:00
feat: differentiate personal/team pricing table with two-stage team workspace flow (#9901)
## 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)
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useCreateWorkspaceUrlLoader } from './useCreateWorkspaceUrlLoader'
|
||||
|
||||
const preservedQueryMocks = vi.hoisted(() => ({
|
||||
clearPreservedQuery: vi.fn(),
|
||||
hydratePreservedQuery: vi.fn(),
|
||||
mergePreservedQueryIntoQuery: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock(
|
||||
'@/platform/navigation/preservedQueryManager',
|
||||
() => preservedQueryMocks
|
||||
)
|
||||
|
||||
const mockRouteQuery = vi.hoisted(() => ({
|
||||
value: {} as Record<string, string>
|
||||
}))
|
||||
const mockRouterReplace = vi.hoisted(() => vi.fn().mockResolvedValue(undefined))
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRoute: () => ({
|
||||
query: mockRouteQuery.value
|
||||
}),
|
||||
useRouter: () => ({
|
||||
replace: mockRouterReplace
|
||||
})
|
||||
}))
|
||||
|
||||
const mockShowTeamWorkspacesDialog = vi.hoisted(() =>
|
||||
vi.fn().mockResolvedValue(undefined)
|
||||
)
|
||||
|
||||
vi.mock('@/services/dialogService', () => ({
|
||||
useDialogService: () => ({
|
||||
showTeamWorkspacesDialog: mockShowTeamWorkspacesDialog
|
||||
})
|
||||
}))
|
||||
|
||||
describe('useCreateWorkspaceUrlLoader', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockRouteQuery.value = {}
|
||||
preservedQueryMocks.mergePreservedQueryIntoQuery.mockReturnValue(null)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('loadCreateWorkspaceFromUrl', () => {
|
||||
it('does nothing when no create_workspace param present', async () => {
|
||||
mockRouteQuery.value = {}
|
||||
|
||||
const { loadCreateWorkspaceFromUrl } = useCreateWorkspaceUrlLoader()
|
||||
await loadCreateWorkspaceFromUrl()
|
||||
|
||||
expect(mockShowTeamWorkspacesDialog).not.toHaveBeenCalled()
|
||||
expect(mockRouterReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('opens create workspace dialog when param is present', async () => {
|
||||
mockRouteQuery.value = { create_workspace: '1' }
|
||||
|
||||
const { loadCreateWorkspaceFromUrl } = useCreateWorkspaceUrlLoader()
|
||||
await loadCreateWorkspaceFromUrl()
|
||||
|
||||
expect(mockShowTeamWorkspacesDialog).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('restores preserved query and opens dialog', async () => {
|
||||
mockRouteQuery.value = {}
|
||||
preservedQueryMocks.mergePreservedQueryIntoQuery.mockReturnValue({
|
||||
create_workspace: '1'
|
||||
})
|
||||
|
||||
const { loadCreateWorkspaceFromUrl } = useCreateWorkspaceUrlLoader()
|
||||
await loadCreateWorkspaceFromUrl()
|
||||
|
||||
expect(preservedQueryMocks.hydratePreservedQuery).toHaveBeenCalledWith(
|
||||
'create_workspace'
|
||||
)
|
||||
expect(mockRouterReplace).toHaveBeenCalledWith({
|
||||
query: { create_workspace: '1' }
|
||||
})
|
||||
expect(mockShowTeamWorkspacesDialog).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('cleans up URL after processing', async () => {
|
||||
mockRouteQuery.value = { create_workspace: '1', other: 'param' }
|
||||
|
||||
const { loadCreateWorkspaceFromUrl } = useCreateWorkspaceUrlLoader()
|
||||
await loadCreateWorkspaceFromUrl()
|
||||
|
||||
expect(mockRouterReplace).toHaveBeenCalledWith({
|
||||
query: { other: 'param' }
|
||||
})
|
||||
})
|
||||
|
||||
it('clears preserved query after processing', async () => {
|
||||
mockRouteQuery.value = { create_workspace: '1' }
|
||||
|
||||
const { loadCreateWorkspaceFromUrl } = useCreateWorkspaceUrlLoader()
|
||||
await loadCreateWorkspaceFromUrl()
|
||||
|
||||
expect(preservedQueryMocks.clearPreservedQuery).toHaveBeenCalledWith(
|
||||
'create_workspace'
|
||||
)
|
||||
})
|
||||
|
||||
it('ignores empty param', async () => {
|
||||
mockRouteQuery.value = { create_workspace: '' }
|
||||
|
||||
const { loadCreateWorkspaceFromUrl } = useCreateWorkspaceUrlLoader()
|
||||
await loadCreateWorkspaceFromUrl()
|
||||
|
||||
expect(mockShowTeamWorkspacesDialog).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('ignores non-string param', async () => {
|
||||
mockRouteQuery.value = {
|
||||
create_workspace: ['array'] as unknown as string
|
||||
}
|
||||
|
||||
const { loadCreateWorkspaceFromUrl } = useCreateWorkspaceUrlLoader()
|
||||
await loadCreateWorkspaceFromUrl()
|
||||
|
||||
expect(mockShowTeamWorkspacesDialog).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user