mirror of
https://github.com/langgenius/dify.git
synced 2026-02-09 23:20:12 -05:00
1351 lines
46 KiB
TypeScript
1351 lines
46 KiB
TypeScript
import type { IconInfo } from '@/models/datasets'
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import * as React from 'react'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import Publisher from './index'
|
|
import Popup from './popup'
|
|
|
|
// ================================
|
|
// Mock External Dependencies Only
|
|
// ================================
|
|
|
|
// Mock next/navigation
|
|
const mockPush = vi.fn()
|
|
vi.mock('next/navigation', () => ({
|
|
useParams: () => ({ datasetId: 'test-dataset-id' }),
|
|
useRouter: () => ({ push: mockPush }),
|
|
}))
|
|
|
|
// Mock next/link
|
|
vi.mock('next/link', () => ({
|
|
default: ({ children, href, ...props }: { children: React.ReactNode, href: string }) => (
|
|
<a href={href} {...props}>{children}</a>
|
|
),
|
|
}))
|
|
|
|
// Mock ahooks
|
|
// Store the keyboard shortcut callback for testing
|
|
let keyPressCallback: ((e: KeyboardEvent) => void) | null = null
|
|
vi.mock('ahooks', () => ({
|
|
useBoolean: (defaultValue = false) => {
|
|
const [value, setValue] = React.useState(defaultValue)
|
|
return [value, {
|
|
setTrue: () => setValue(true),
|
|
setFalse: () => setValue(false),
|
|
toggle: () => setValue(v => !v),
|
|
}]
|
|
},
|
|
useKeyPress: (key: string, callback: (e: KeyboardEvent) => void) => {
|
|
// Store the callback so we can invoke it in tests
|
|
keyPressCallback = callback
|
|
},
|
|
}))
|
|
|
|
// Mock amplitude tracking
|
|
vi.mock('@/app/components/base/amplitude', () => ({
|
|
trackEvent: vi.fn(),
|
|
}))
|
|
|
|
// Mock portal-to-follow-elem
|
|
let mockPortalOpen = false
|
|
vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
|
|
PortalToFollowElem: ({ children, open, onOpenChange: _onOpenChange }: {
|
|
children: React.ReactNode
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
}) => {
|
|
mockPortalOpen = open
|
|
return <div data-testid="portal-elem" data-open={open}>{children}</div>
|
|
},
|
|
PortalToFollowElemTrigger: ({ children, onClick }: {
|
|
children: React.ReactNode
|
|
onClick: () => void
|
|
}) => (
|
|
<div data-testid="portal-trigger" onClick={onClick}>
|
|
{children}
|
|
</div>
|
|
),
|
|
PortalToFollowElemContent: ({ children, className }: {
|
|
children: React.ReactNode
|
|
className?: string
|
|
}) => {
|
|
if (!mockPortalOpen)
|
|
return null
|
|
return <div data-testid="portal-content" className={className}>{children}</div>
|
|
},
|
|
}))
|
|
|
|
// Mock workflow hooks
|
|
const mockHandleSyncWorkflowDraft = vi.fn()
|
|
const mockHandleCheckBeforePublish = vi.fn().mockResolvedValue(true)
|
|
vi.mock('@/app/components/workflow/hooks', () => ({
|
|
useNodesSyncDraft: () => ({
|
|
handleSyncWorkflowDraft: mockHandleSyncWorkflowDraft,
|
|
}),
|
|
useChecklistBeforePublish: () => ({
|
|
handleCheckBeforePublish: mockHandleCheckBeforePublish,
|
|
}),
|
|
}))
|
|
|
|
// Mock workflow store
|
|
const mockPublishedAt = vi.fn(() => null as number | null)
|
|
const mockDraftUpdatedAt = vi.fn(() => 1700000000)
|
|
const mockPipelineId = vi.fn(() => 'test-pipeline-id')
|
|
const mockSetPublishedAt = vi.fn()
|
|
|
|
vi.mock('@/app/components/workflow/store', () => ({
|
|
useStore: (selector: (s: Record<string, unknown>) => unknown) => {
|
|
const state = {
|
|
publishedAt: mockPublishedAt(),
|
|
draftUpdatedAt: mockDraftUpdatedAt(),
|
|
pipelineId: mockPipelineId(),
|
|
}
|
|
return selector(state)
|
|
},
|
|
useWorkflowStore: () => ({
|
|
getState: () => ({
|
|
setPublishedAt: mockSetPublishedAt,
|
|
}),
|
|
}),
|
|
}))
|
|
|
|
// Mock dataset-detail context
|
|
const mockMutateDatasetRes = vi.fn()
|
|
vi.mock('@/context/dataset-detail', () => ({
|
|
useDatasetDetailContextWithSelector: (selector: (s: Record<string, unknown>) => unknown) => {
|
|
const state = { mutateDatasetRes: mockMutateDatasetRes }
|
|
return selector(state)
|
|
},
|
|
}))
|
|
|
|
// Mock modal-context
|
|
const mockSetShowPricingModal = vi.fn()
|
|
vi.mock('@/context/modal-context', () => ({
|
|
useModalContextSelector: () => mockSetShowPricingModal,
|
|
}))
|
|
|
|
// Mock provider-context
|
|
const mockIsAllowPublishAsCustomKnowledgePipelineTemplate = vi.fn(() => true)
|
|
vi.mock('@/context/provider-context', () => ({
|
|
useProviderContext: () => ({
|
|
isAllowPublishAsCustomKnowledgePipelineTemplate: mockIsAllowPublishAsCustomKnowledgePipelineTemplate(),
|
|
}),
|
|
useProviderContextSelector: <T,>(selector: (s: { isAllowPublishAsCustomKnowledgePipelineTemplate: boolean }) => T): T =>
|
|
selector({ isAllowPublishAsCustomKnowledgePipelineTemplate: mockIsAllowPublishAsCustomKnowledgePipelineTemplate() }),
|
|
}))
|
|
|
|
// Mock toast context
|
|
const mockNotify = vi.fn()
|
|
vi.mock('@/app/components/base/toast', () => ({
|
|
useToastContext: () => ({
|
|
notify: mockNotify,
|
|
}),
|
|
}))
|
|
|
|
// Mock API access URL hook
|
|
vi.mock('@/hooks/use-api-access-url', () => ({
|
|
useDatasetApiAccessUrl: () => 'https://api.dify.ai/v1/datasets/test-dataset-id',
|
|
}))
|
|
|
|
// Mock format time hook
|
|
vi.mock('@/hooks/use-format-time-from-now', () => ({
|
|
useFormatTimeFromNow: () => ({
|
|
formatTimeFromNow: (timestamp: number) => {
|
|
const diff = Date.now() / 1000 - timestamp
|
|
if (diff < 60)
|
|
return 'just now'
|
|
if (diff < 3600)
|
|
return `${Math.floor(diff / 60)} minutes ago`
|
|
return new Date(timestamp * 1000).toLocaleDateString()
|
|
},
|
|
}),
|
|
}))
|
|
|
|
// Mock service hooks
|
|
const mockPublishWorkflow = vi.fn()
|
|
const mockPublishAsCustomizedPipeline = vi.fn()
|
|
const mockInvalidPublishedPipelineInfo = vi.fn()
|
|
const mockInvalidDatasetList = vi.fn()
|
|
const mockInvalidCustomizedTemplateList = vi.fn()
|
|
|
|
vi.mock('@/service/knowledge/use-dataset', () => ({
|
|
useInvalidDatasetList: () => mockInvalidDatasetList,
|
|
}))
|
|
|
|
vi.mock('@/service/use-base', () => ({
|
|
useInvalid: () => mockInvalidPublishedPipelineInfo,
|
|
}))
|
|
|
|
vi.mock('@/service/use-pipeline', () => ({
|
|
publishedPipelineInfoQueryKeyPrefix: ['pipeline', 'published'],
|
|
useInvalidCustomizedTemplateList: () => mockInvalidCustomizedTemplateList,
|
|
usePublishAsCustomizedPipeline: () => ({
|
|
mutateAsync: mockPublishAsCustomizedPipeline,
|
|
}),
|
|
}))
|
|
|
|
vi.mock('@/service/use-workflow', () => ({
|
|
usePublishWorkflow: () => ({
|
|
mutateAsync: mockPublishWorkflow,
|
|
}),
|
|
}))
|
|
|
|
// Mock workflow utils
|
|
vi.mock('@/app/components/workflow/utils', () => ({
|
|
getKeyboardKeyCodeBySystem: (key: string) => key,
|
|
getKeyboardKeyNameBySystem: (key: string) => key === 'ctrl' ? '⌘' : key,
|
|
}))
|
|
|
|
// Mock PublishAsKnowledgePipelineModal
|
|
vi.mock('../../publish-as-knowledge-pipeline-modal', () => ({
|
|
default: ({ confirmDisabled, onConfirm, onCancel }: {
|
|
confirmDisabled: boolean
|
|
onConfirm: (name: string, icon: IconInfo, description?: string) => void
|
|
onCancel: () => void
|
|
}) => (
|
|
<div data-testid="publish-as-knowledge-pipeline-modal">
|
|
<button
|
|
data-testid="modal-confirm"
|
|
disabled={confirmDisabled}
|
|
onClick={() => onConfirm('Test Pipeline', { type: 'emoji', emoji: '📚', background: '#fff' } as unknown as IconInfo, 'Test description')}
|
|
>
|
|
Confirm
|
|
</button>
|
|
<button data-testid="modal-cancel" onClick={onCancel}>Cancel</button>
|
|
</div>
|
|
),
|
|
}))
|
|
|
|
// ================================
|
|
// Test Data Factories
|
|
// ================================
|
|
|
|
const createQueryClient = () => new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
retry: false,
|
|
},
|
|
},
|
|
})
|
|
|
|
const renderWithQueryClient = (ui: React.ReactElement) => {
|
|
const queryClient = createQueryClient()
|
|
return render(
|
|
<QueryClientProvider client={queryClient}>
|
|
{ui}
|
|
</QueryClientProvider>,
|
|
)
|
|
}
|
|
|
|
// ================================
|
|
// Test Suites
|
|
// ================================
|
|
|
|
describe('publisher', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockPortalOpen = false
|
|
keyPressCallback = null
|
|
// Reset mock return values to defaults
|
|
mockPublishedAt.mockReturnValue(null)
|
|
mockDraftUpdatedAt.mockReturnValue(1700000000)
|
|
mockPipelineId.mockReturnValue('test-pipeline-id')
|
|
mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(true)
|
|
mockHandleCheckBeforePublish.mockResolvedValue(true)
|
|
})
|
|
|
|
// ============================================================
|
|
// Publisher (index.tsx) - Main Entry Component Tests
|
|
// ============================================================
|
|
describe('Publisher (index.tsx)', () => {
|
|
// --------------------------------
|
|
// Rendering Tests
|
|
// --------------------------------
|
|
describe('Rendering', () => {
|
|
it('should render publish button with correct text', () => {
|
|
// Arrange & Act
|
|
renderWithQueryClient(<Publisher />)
|
|
|
|
// Assert
|
|
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
expect(screen.getByText('workflow.common.publish')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render portal element in closed state by default', () => {
|
|
// Arrange & Act
|
|
renderWithQueryClient(<Publisher />)
|
|
|
|
// Assert
|
|
expect(screen.getByTestId('portal-elem')).toHaveAttribute('data-open', 'false')
|
|
expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should render down arrow icon in button', () => {
|
|
// Arrange & Act
|
|
renderWithQueryClient(<Publisher />)
|
|
|
|
// Assert
|
|
const button = screen.getByRole('button')
|
|
expect(button.querySelector('svg')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// --------------------------------
|
|
// State Management Tests
|
|
// --------------------------------
|
|
describe('State Management', () => {
|
|
it('should open popup when trigger is clicked', async () => {
|
|
// Arrange
|
|
renderWithQueryClient(<Publisher />)
|
|
|
|
// Act
|
|
fireEvent.click(screen.getByTestId('portal-trigger'))
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('portal-content')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should close popup when trigger is clicked again while open', async () => {
|
|
// Arrange
|
|
renderWithQueryClient(<Publisher />)
|
|
fireEvent.click(screen.getByTestId('portal-trigger')) // open
|
|
|
|
// Act
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('portal-content')).toBeInTheDocument()
|
|
})
|
|
fireEvent.click(screen.getByTestId('portal-trigger')) // close
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// --------------------------------
|
|
// Callback Stability and Memoization Tests
|
|
// --------------------------------
|
|
describe('Callback Stability and Memoization', () => {
|
|
it('should call handleSyncWorkflowDraft when popup opens', async () => {
|
|
// Arrange
|
|
renderWithQueryClient(<Publisher />)
|
|
|
|
// Act
|
|
fireEvent.click(screen.getByTestId('portal-trigger'))
|
|
|
|
// Assert
|
|
expect(mockHandleSyncWorkflowDraft).toHaveBeenCalledWith(true)
|
|
})
|
|
|
|
it('should not call handleSyncWorkflowDraft when popup closes', async () => {
|
|
// Arrange
|
|
renderWithQueryClient(<Publisher />)
|
|
fireEvent.click(screen.getByTestId('portal-trigger')) // open
|
|
vi.clearAllMocks()
|
|
|
|
// Act
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('portal-content')).toBeInTheDocument()
|
|
})
|
|
fireEvent.click(screen.getByTestId('portal-trigger')) // close
|
|
|
|
// Assert
|
|
expect(mockHandleSyncWorkflowDraft).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should be memoized with React.memo', () => {
|
|
// Assert
|
|
expect(Publisher).toBeDefined()
|
|
expect((Publisher as unknown as { $$typeof?: symbol }).$$typeof?.toString()).toContain('Symbol')
|
|
})
|
|
})
|
|
|
|
// --------------------------------
|
|
// User Interactions Tests
|
|
// --------------------------------
|
|
describe('User Interactions', () => {
|
|
it('should render popup content when opened', async () => {
|
|
// Arrange
|
|
renderWithQueryClient(<Publisher />)
|
|
|
|
// Act
|
|
fireEvent.click(screen.getByTestId('portal-trigger'))
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('portal-content')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Popup (popup.tsx) - Main Popup Component Tests
|
|
// ============================================================
|
|
describe('Popup (popup.tsx)', () => {
|
|
// --------------------------------
|
|
// Rendering Tests
|
|
// --------------------------------
|
|
describe('Rendering', () => {
|
|
it('should render unpublished state when publishedAt is null', () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(null)
|
|
|
|
// Act
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Assert
|
|
expect(screen.getByText('workflow.common.currentDraftUnpublished')).toBeInTheDocument()
|
|
expect(screen.getByText(/workflow.common.autoSaved/)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render published state when publishedAt has value', () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
|
|
// Act
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Assert
|
|
expect(screen.getByText('workflow.common.latestPublished')).toBeInTheDocument()
|
|
expect(screen.getByText(/workflow.common.publishedAt/)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render publish button with keyboard shortcuts', () => {
|
|
// Arrange & Act
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Assert
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
expect(publishButton).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render action buttons section', () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
|
|
// Act
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Assert
|
|
expect(screen.getByText('pipeline.common.goToAddDocuments')).toBeInTheDocument()
|
|
expect(screen.getByText('workflow.common.accessAPIReference')).toBeInTheDocument()
|
|
expect(screen.getByText('pipeline.common.publishAs')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should disable action buttons when not published', () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(null)
|
|
|
|
// Act
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Assert
|
|
const addDocumentsButton = screen.getAllByRole('button').find(btn =>
|
|
btn.textContent?.includes('pipeline.common.goToAddDocuments'),
|
|
)
|
|
expect(addDocumentsButton).toBeDisabled()
|
|
})
|
|
|
|
it('should enable action buttons when published', () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
|
|
// Act
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Assert
|
|
const addDocumentsButton = screen.getAllByRole('button').find(btn =>
|
|
btn.textContent?.includes('pipeline.common.goToAddDocuments'),
|
|
)
|
|
expect(addDocumentsButton).not.toBeDisabled()
|
|
})
|
|
|
|
it('should show premium badge when publish as template is not allowed', () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(false)
|
|
|
|
// Act
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Assert
|
|
expect(screen.getByText('billing.upgradeBtn.encourageShort')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not show premium badge when publish as template is allowed', () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(true)
|
|
|
|
// Act
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Assert
|
|
expect(screen.queryByText('billing.upgradeBtn.encourageShort')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// --------------------------------
|
|
// State Management Tests
|
|
// --------------------------------
|
|
describe('State Management', () => {
|
|
it('should show confirm modal when first publish attempt on unpublished pipeline', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(null)
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
fireEvent.click(publishButton)
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(screen.getByText('pipeline.common.confirmPublish')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should not show confirm modal when already published', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 })
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
fireEvent.click(publishButton)
|
|
|
|
// Assert - should call publish directly without confirm
|
|
await waitFor(() => {
|
|
expect(mockPublishWorkflow).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should update to published state after successful publish', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 })
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
fireEvent.click(publishButton)
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: /workflow.common.published/i })).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// --------------------------------
|
|
// User Interactions Tests
|
|
// --------------------------------
|
|
describe('User Interactions', () => {
|
|
it('should navigate to add documents when go to add documents is clicked', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act
|
|
const addDocumentsButton = screen.getAllByRole('button').find(btn =>
|
|
btn.textContent?.includes('pipeline.common.goToAddDocuments'),
|
|
)
|
|
fireEvent.click(addDocumentsButton!)
|
|
|
|
// Assert
|
|
expect(mockPush).toHaveBeenCalledWith('/datasets/test-dataset-id/documents/create-from-pipeline')
|
|
})
|
|
|
|
it('should show pricing modal when publish as template is clicked without permission', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(false)
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act
|
|
const publishAsButton = screen.getAllByRole('button').find(btn =>
|
|
btn.textContent?.includes('pipeline.common.publishAs'),
|
|
)
|
|
fireEvent.click(publishAsButton!)
|
|
|
|
// Assert
|
|
expect(mockSetShowPricingModal).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should show publish as knowledge pipeline modal when permitted', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(true)
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act
|
|
const publishAsButton = screen.getAllByRole('button').find(btn =>
|
|
btn.textContent?.includes('pipeline.common.publishAs'),
|
|
)
|
|
fireEvent.click(publishAsButton!)
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('publish-as-knowledge-pipeline-modal')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should close publish as knowledge pipeline modal when cancel is clicked', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(true)
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
const publishAsButton = screen.getAllByRole('button').find(btn =>
|
|
btn.textContent?.includes('pipeline.common.publishAs'),
|
|
)
|
|
fireEvent.click(publishAsButton!)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('publish-as-knowledge-pipeline-modal')).toBeInTheDocument()
|
|
})
|
|
|
|
// Act
|
|
fireEvent.click(screen.getByTestId('modal-cancel'))
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(screen.queryByTestId('publish-as-knowledge-pipeline-modal')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should call publishAsCustomizedPipeline when confirm is clicked in modal', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockPublishAsCustomizedPipeline.mockResolvedValue({})
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
const publishAsButton = screen.getAllByRole('button').find(btn =>
|
|
btn.textContent?.includes('pipeline.common.publishAs'),
|
|
)
|
|
fireEvent.click(publishAsButton!)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('publish-as-knowledge-pipeline-modal')).toBeInTheDocument()
|
|
})
|
|
|
|
// Act
|
|
fireEvent.click(screen.getByTestId('modal-confirm'))
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(mockPublishAsCustomizedPipeline).toHaveBeenCalledWith({
|
|
pipelineId: 'test-pipeline-id',
|
|
name: 'Test Pipeline',
|
|
icon_info: { type: 'emoji', emoji: '📚', background: '#fff' },
|
|
description: 'Test description',
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// --------------------------------
|
|
// API Calls and Async Operations Tests
|
|
// --------------------------------
|
|
describe('API Calls and Async Operations', () => {
|
|
it('should call publishWorkflow API when publish button is clicked', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 })
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
fireEvent.click(publishButton)
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(mockPublishWorkflow).toHaveBeenCalledWith({
|
|
url: '/rag/pipelines/test-pipeline-id/workflows/publish',
|
|
title: '',
|
|
releaseNotes: '',
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should show success notification after publish', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 })
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
fireEvent.click(publishButton)
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(mockNotify).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
type: 'success',
|
|
message: 'datasetPipeline.publishPipeline.success.message',
|
|
}),
|
|
)
|
|
})
|
|
})
|
|
|
|
it('should update publishedAt in store after successful publish', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 })
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
fireEvent.click(publishButton)
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(mockSetPublishedAt).toHaveBeenCalledWith(1700100000)
|
|
})
|
|
})
|
|
|
|
it('should invalidate caches after successful publish', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 })
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
fireEvent.click(publishButton)
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(mockMutateDatasetRes).toHaveBeenCalled()
|
|
expect(mockInvalidPublishedPipelineInfo).toHaveBeenCalled()
|
|
expect(mockInvalidDatasetList).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should show success notification for publish as template', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockPublishAsCustomizedPipeline.mockResolvedValue({})
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
const publishAsButton = screen.getAllByRole('button').find(btn =>
|
|
btn.textContent?.includes('pipeline.common.publishAs'),
|
|
)
|
|
fireEvent.click(publishAsButton!)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('publish-as-knowledge-pipeline-modal')).toBeInTheDocument()
|
|
})
|
|
|
|
// Act
|
|
fireEvent.click(screen.getByTestId('modal-confirm'))
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(mockNotify).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
type: 'success',
|
|
message: 'datasetPipeline.publishTemplate.success.message',
|
|
}),
|
|
)
|
|
})
|
|
})
|
|
|
|
it('should invalidate customized template list after publish as template', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockPublishAsCustomizedPipeline.mockResolvedValue({})
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
const publishAsButton = screen.getAllByRole('button').find(btn =>
|
|
btn.textContent?.includes('pipeline.common.publishAs'),
|
|
)
|
|
fireEvent.click(publishAsButton!)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('publish-as-knowledge-pipeline-modal')).toBeInTheDocument()
|
|
})
|
|
|
|
// Act
|
|
fireEvent.click(screen.getByTestId('modal-confirm'))
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(mockInvalidCustomizedTemplateList).toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|
|
|
|
// --------------------------------
|
|
// Error Handling Tests
|
|
// --------------------------------
|
|
describe('Error Handling', () => {
|
|
it('should not proceed with publish when check fails', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockHandleCheckBeforePublish.mockResolvedValue(false)
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
fireEvent.click(publishButton)
|
|
|
|
// Assert - publishWorkflow should not be called when check fails
|
|
await waitFor(() => {
|
|
expect(mockHandleCheckBeforePublish).toHaveBeenCalled()
|
|
})
|
|
expect(mockPublishWorkflow).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should show error notification when publish fails', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockPublishWorkflow.mockRejectedValue(new Error('Publish failed'))
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
fireEvent.click(publishButton)
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(mockNotify).toHaveBeenCalledWith({
|
|
type: 'error',
|
|
message: 'datasetPipeline.publishPipeline.error.message',
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should show error notification when publish as template fails', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockPublishAsCustomizedPipeline.mockRejectedValue(new Error('Template publish failed'))
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
const publishAsButton = screen.getAllByRole('button').find(btn =>
|
|
btn.textContent?.includes('pipeline.common.publishAs'),
|
|
)
|
|
fireEvent.click(publishAsButton!)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('publish-as-knowledge-pipeline-modal')).toBeInTheDocument()
|
|
})
|
|
|
|
// Act
|
|
fireEvent.click(screen.getByTestId('modal-confirm'))
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(mockNotify).toHaveBeenCalledWith({
|
|
type: 'error',
|
|
message: 'datasetPipeline.publishTemplate.error.message',
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should close modal after publish as template error', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockPublishAsCustomizedPipeline.mockRejectedValue(new Error('Template publish failed'))
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
const publishAsButton = screen.getAllByRole('button').find(btn =>
|
|
btn.textContent?.includes('pipeline.common.publishAs'),
|
|
)
|
|
fireEvent.click(publishAsButton!)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('publish-as-knowledge-pipeline-modal')).toBeInTheDocument()
|
|
})
|
|
|
|
// Act
|
|
fireEvent.click(screen.getByTestId('modal-confirm'))
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(screen.queryByTestId('publish-as-knowledge-pipeline-modal')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
// --------------------------------
|
|
// Confirm Modal Tests
|
|
// --------------------------------
|
|
describe('Confirm Modal', () => {
|
|
it('should hide confirm modal when cancel is clicked', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(null)
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
fireEvent.click(publishButton)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('pipeline.common.confirmPublish')).toBeInTheDocument()
|
|
})
|
|
|
|
// Act - find and click cancel button in confirm modal
|
|
const cancelButtons = screen.getAllByRole('button')
|
|
const cancelButton = cancelButtons.find(btn =>
|
|
btn.className.includes('cancel') || btn.textContent?.includes('Cancel'),
|
|
)
|
|
if (cancelButton)
|
|
fireEvent.click(cancelButton)
|
|
|
|
// Trigger onCancel manually since we can't find the exact button
|
|
// The Confirm component has an onCancel prop that calls hideConfirm
|
|
|
|
// Assert - modal should be dismissable
|
|
// Note: This test verifies the confirm modal can be displayed
|
|
expect(screen.getByText('pipeline.common.confirmPublishContent')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should publish when confirm is clicked in confirm modal', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(null)
|
|
mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 })
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
fireEvent.click(publishButton) // This shows confirm modal
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('pipeline.common.confirmPublish')).toBeInTheDocument()
|
|
})
|
|
|
|
// Assert - confirm modal content is displayed
|
|
expect(screen.getByText('pipeline.common.confirmPublishContent')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// --------------------------------
|
|
// Component Memoization Tests
|
|
// --------------------------------
|
|
describe('Component Memoization', () => {
|
|
it('should be memoized with React.memo', () => {
|
|
// Assert
|
|
expect(Popup).toBeDefined()
|
|
expect((Popup as unknown as { $$typeof?: symbol }).$$typeof?.toString()).toContain('Symbol')
|
|
})
|
|
})
|
|
|
|
// --------------------------------
|
|
// Prop Variations Tests
|
|
// --------------------------------
|
|
describe('Prop Variations', () => {
|
|
it('should display correct width when permission is allowed', () => {
|
|
// Test with permission
|
|
mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(true)
|
|
const { container } = renderWithQueryClient(<Popup />)
|
|
|
|
const popupDiv = container.firstChild as HTMLElement
|
|
expect(popupDiv.className).toContain('w-[360px]')
|
|
})
|
|
|
|
it('should display correct width when permission is not allowed', () => {
|
|
// Test without permission
|
|
mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(false)
|
|
const { container } = renderWithQueryClient(<Popup />)
|
|
|
|
const popupDiv = container.firstChild as HTMLElement
|
|
expect(popupDiv.className).toContain('w-[400px]')
|
|
})
|
|
|
|
it('should display draft updated time when not published', () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(null)
|
|
mockDraftUpdatedAt.mockReturnValue(1700000000)
|
|
|
|
// Act
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Assert
|
|
expect(screen.getByText(/workflow.common.autoSaved/)).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle null draftUpdatedAt gracefully', () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(null)
|
|
mockDraftUpdatedAt.mockReturnValue(0)
|
|
|
|
// Act
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Assert
|
|
expect(screen.getByText(/workflow.common.autoSaved/)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// --------------------------------
|
|
// API Reference Link Tests
|
|
// --------------------------------
|
|
describe('API Reference Link', () => {
|
|
it('should render API reference link with correct href', () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
|
|
// Act
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Assert
|
|
const apiLink = screen.getByRole('link')
|
|
expect(apiLink).toHaveAttribute('href', 'https://api.dify.ai/v1/datasets/test-dataset-id')
|
|
expect(apiLink).toHaveAttribute('target', '_blank')
|
|
})
|
|
})
|
|
|
|
// --------------------------------
|
|
// Keyboard Shortcut Tests
|
|
// --------------------------------
|
|
describe('Keyboard Shortcuts', () => {
|
|
it('should trigger publish when keyboard shortcut is pressed', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 })
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act - simulate keyboard shortcut
|
|
const mockEvent = { preventDefault: vi.fn() } as unknown as KeyboardEvent
|
|
keyPressCallback?.(mockEvent)
|
|
|
|
// Assert
|
|
expect(mockEvent.preventDefault).toHaveBeenCalled()
|
|
await waitFor(() => {
|
|
expect(mockPublishWorkflow).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should not trigger publish when already published in session', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 })
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// First publish via button click to set published state
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
fireEvent.click(publishButton)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: /workflow.common.published/i })).toBeInTheDocument()
|
|
})
|
|
|
|
vi.clearAllMocks()
|
|
|
|
// Act - simulate keyboard shortcut after already published
|
|
const mockEvent = { preventDefault: vi.fn() } as unknown as KeyboardEvent
|
|
keyPressCallback?.(mockEvent)
|
|
|
|
// Assert - should return early without publishing
|
|
expect(mockEvent.preventDefault).toHaveBeenCalled()
|
|
expect(mockPublishWorkflow).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should show confirm modal when shortcut pressed on unpublished pipeline', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(null)
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act - simulate keyboard shortcut
|
|
const mockEvent = { preventDefault: vi.fn() } as unknown as KeyboardEvent
|
|
keyPressCallback?.(mockEvent)
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(screen.getByText('pipeline.common.confirmPublish')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should not trigger duplicate publish via shortcut when already publishing', async () => {
|
|
// Arrange - create a promise that we can control
|
|
let resolvePublish: () => void = () => {}
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockPublishWorkflow.mockImplementation(() => new Promise((resolve) => {
|
|
resolvePublish = () => resolve({ created_at: 1700100000 })
|
|
}))
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act - trigger publish via keyboard shortcut first
|
|
const mockEvent1 = { preventDefault: vi.fn() } as unknown as KeyboardEvent
|
|
keyPressCallback?.(mockEvent1)
|
|
|
|
// Wait for the first publish to start (button becomes disabled)
|
|
await waitFor(() => {
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
expect(publishButton).toBeDisabled()
|
|
})
|
|
|
|
// Try to trigger again via shortcut while publishing
|
|
const mockEvent2 = { preventDefault: vi.fn() } as unknown as KeyboardEvent
|
|
keyPressCallback?.(mockEvent2)
|
|
|
|
// Assert - only one call to publishWorkflow
|
|
expect(mockPublishWorkflow).toHaveBeenCalledTimes(1)
|
|
|
|
// Cleanup - resolve the promise
|
|
resolvePublish()
|
|
})
|
|
})
|
|
|
|
// --------------------------------
|
|
// Finally Block Cleanup Tests
|
|
// --------------------------------
|
|
describe('Finally Block Cleanup', () => {
|
|
it('should reset publishing state after successful publish', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 })
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
fireEvent.click(publishButton)
|
|
|
|
// Assert - button should be disabled during publishing, then show published
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: /workflow.common.published/i })).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should reset publishing state after failed publish', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockPublishWorkflow.mockRejectedValue(new Error('Publish failed'))
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
fireEvent.click(publishButton)
|
|
|
|
// Assert - should show error and button should be enabled again (not showing "published")
|
|
await waitFor(() => {
|
|
expect(mockNotify).toHaveBeenCalledWith({
|
|
type: 'error',
|
|
message: 'datasetPipeline.publishPipeline.error.message',
|
|
})
|
|
})
|
|
|
|
// Button should still show publishUpdate since it wasn't successfully published
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: /workflow.common.publishUpdate/i })).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should hide confirm modal after publish from confirm', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(null)
|
|
mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 })
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Show confirm modal first
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
fireEvent.click(publishButton)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('pipeline.common.confirmPublish')).toBeInTheDocument()
|
|
})
|
|
|
|
// Act - trigger publish again (which happens when confirm is clicked)
|
|
// The mock for workflow hooks returns handleCheckBeforePublish that resolves to true
|
|
// We need to simulate the confirm button click which calls handlePublish again
|
|
// Since confirmVisible is now true and publishedAt is null, it should proceed to publish
|
|
fireEvent.click(publishButton)
|
|
|
|
// Assert - confirm modal should be hidden after publish completes
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: /workflow.common.published/i })).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should hide confirm modal after failed publish', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(null)
|
|
mockPublishWorkflow.mockRejectedValue(new Error('Publish failed'))
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Show confirm modal first
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
fireEvent.click(publishButton)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('pipeline.common.confirmPublish')).toBeInTheDocument()
|
|
})
|
|
|
|
// Act - trigger publish from confirm (call handlePublish when confirmVisible is true)
|
|
fireEvent.click(publishButton)
|
|
|
|
// Assert - error notification should be shown
|
|
await waitFor(() => {
|
|
expect(mockNotify).toHaveBeenCalledWith({
|
|
type: 'error',
|
|
message: 'datasetPipeline.publishPipeline.error.message',
|
|
})
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Edge Cases
|
|
// ============================================================
|
|
describe('Edge Cases', () => {
|
|
it('should handle undefined pipelineId gracefully', () => {
|
|
// Arrange
|
|
mockPipelineId.mockReturnValue('')
|
|
|
|
// Act
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Assert - should render without crashing
|
|
expect(screen.getByText('workflow.common.currentDraftUnpublished')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle empty publish response', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockPublishWorkflow.mockResolvedValue(null)
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
fireEvent.click(publishButton)
|
|
|
|
// Assert - should not call setPublishedAt or notify when response is null
|
|
await waitFor(() => {
|
|
expect(mockPublishWorkflow).toHaveBeenCalled()
|
|
})
|
|
// setPublishedAt should not be called because res is falsy
|
|
expect(mockSetPublishedAt).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('should prevent multiple simultaneous publish calls', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
// Create a promise that never resolves to simulate ongoing publish
|
|
mockPublishWorkflow.mockImplementation(() => new Promise(() => {}))
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act - click publish button multiple times rapidly
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
fireEvent.click(publishButton)
|
|
|
|
// Wait for button to become disabled
|
|
await waitFor(() => {
|
|
expect(publishButton).toBeDisabled()
|
|
})
|
|
|
|
// Try clicking again
|
|
fireEvent.click(publishButton)
|
|
fireEvent.click(publishButton)
|
|
|
|
// Assert - publishWorkflow should only be called once due to guard
|
|
expect(mockPublishWorkflow).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('should disable publish button when already published in session', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 })
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act - publish once
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
fireEvent.click(publishButton)
|
|
|
|
// Assert - button should show "published" state
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('button', { name: /workflow.common.published/i })).toBeDisabled()
|
|
})
|
|
})
|
|
|
|
it('should not trigger publish when already publishing', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockPublishWorkflow.mockImplementation(() => new Promise(() => {})) // Never resolves
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
fireEvent.click(publishButton)
|
|
|
|
// The button should be disabled while publishing
|
|
await waitFor(() => {
|
|
expect(publishButton).toBeDisabled()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================
|
|
// Integration Tests
|
|
// ============================================================
|
|
describe('Integration Tests', () => {
|
|
it('should complete full publish flow for unpublished pipeline', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(null)
|
|
mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 })
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act - click publish to show confirm
|
|
const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i })
|
|
fireEvent.click(publishButton)
|
|
|
|
// Assert - confirm modal should appear
|
|
await waitFor(() => {
|
|
expect(screen.getByText('pipeline.common.confirmPublish')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should complete full publish as template flow', async () => {
|
|
// Arrange
|
|
mockPublishedAt.mockReturnValue(1700000000)
|
|
mockPublishAsCustomizedPipeline.mockResolvedValue({})
|
|
renderWithQueryClient(<Popup />)
|
|
|
|
// Act - click publish as template button
|
|
const publishAsButton = screen.getAllByRole('button').find(btn =>
|
|
btn.textContent?.includes('pipeline.common.publishAs'),
|
|
)
|
|
fireEvent.click(publishAsButton!)
|
|
|
|
// Assert - modal should appear
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('publish-as-knowledge-pipeline-modal')).toBeInTheDocument()
|
|
})
|
|
|
|
// Act - confirm
|
|
fireEvent.click(screen.getByTestId('modal-confirm'))
|
|
|
|
// Assert - success notification and modal closes
|
|
await waitFor(() => {
|
|
expect(mockNotify).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
type: 'success',
|
|
}),
|
|
)
|
|
expect(screen.queryByTestId('publish-as-knowledge-pipeline-modal')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should show Publisher button and open popup with Popup component', async () => {
|
|
// Arrange & Act
|
|
renderWithQueryClient(<Publisher />)
|
|
|
|
// Click to open popup
|
|
fireEvent.click(screen.getByTestId('portal-trigger'))
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('portal-content')).toBeInTheDocument()
|
|
})
|
|
|
|
// Verify sync was called when opening
|
|
expect(mockHandleSyncWorkflowDraft).toHaveBeenCalledWith(true)
|
|
})
|
|
})
|
|
})
|