import type { ReactNode } from 'react' import type { App } from '@/models/explore' import type { TryAppSelection } from '@/types/try-app' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' import { useContextSelector } from 'use-context-selector' import AppListContext from '@/context/app-list-context' import { fetchAppDetail } from '@/service/explore' import { render } from '@/test/console/render' import { AppModeEnum } from '@/types/app' import Apps from '../index' vi.mock('@/next/dynamic', () => ({ default: (loader: () => Promise<{ default: React.ComponentType }>) => { const LazyComp = React.lazy(loader) return function DynamicWrapper(props: Record) { return React.createElement( React.Suspense, { fallback: null }, React.createElement(LazyComp, props), ) } }, })) let documentTitleCalls: string[] = [] let educationInitCalls: number = 0 const mockHandleImportDSL = vi.fn() const mockHandleImportDSLConfirm = vi.fn() const mockTrackCreateApp = vi.fn() const mockFetchAppDetail = vi.mocked(fetchAppDetail) let mockWorkspacePermissionKeys: string[] = ['app.create_and_management'] const mockTemplateApp: App = { app_id: 'template-1', categories: ['Assistant'], app: { id: 'template-1', mode: AppModeEnum.CHAT, icon_type: 'emoji', icon: '🤖', icon_background: '#fff', icon_url: '', name: 'Sample App', description: 'Sample App', use_icon_as_answer_icon: false, }, description: 'Sample App', can_trial: true, copyright: '', privacy_policy: null, custom_disclaimer: null, position: 1, is_listed: true, install_count: 0, installed: false, editable: false, is_agent: false, } vi.mock('@/hooks/use-document-title', () => ({ default: (title: string) => { documentTitleCalls.push(title) }, })) vi.mock('@/app/education-apply/hooks', () => ({ useEducationInit: () => { educationInitCalls++ }, })) vi.mock('@/context/permission-state', async () => { const { createPermissionStateModuleMock } = await import('@/test/console/state-fixture') return createPermissionStateModuleMock(() => ({ workspacePermissionKeys: mockWorkspacePermissionKeys, })) }) vi.mock('@/hooks/use-import-dsl', () => ({ useImportDSL: () => ({ handleImportDSL: mockHandleImportDSL, handleImportDSLConfirm: mockHandleImportDSLConfirm, versions: [], isFetching: false, }), })) const mockReplace = vi.fn() let mockSearchParams = new URLSearchParams() vi.mock('@/next/navigation', () => ({ useRouter: () => ({ replace: mockReplace, }), useSearchParams: () => mockSearchParams, })) vi.mock('../list', () => { const MockList = ({ onCreateLearnDify, onTryLearnDify, }: { onCreateLearnDify?: (app: App) => void onTryLearnDify?: (params: TryAppSelection) => void }) => { const setShowTryAppPanel = useContextSelector(AppListContext, (ctx) => ctx.setShowTryAppPanel) return React.createElement( 'div', { 'data-testid': 'apps-list' }, React.createElement('span', null, 'Apps List'), React.createElement( 'button', { 'data-testid': 'open-preview', onClick: () => setShowTryAppPanel(true, { appId: mockTemplateApp.app_id, app: mockTemplateApp, }), }, 'Open Preview', ), React.createElement( 'button', { type: 'button', onClick: () => onTryLearnDify?.({ appId: mockTemplateApp.app_id, app: mockTemplateApp }), }, 'Preview Learn Dify template', ), React.createElement( 'button', { type: 'button', onClick: () => onCreateLearnDify?.(mockTemplateApp) }, 'Create Learn Dify template', ), ) } return { default: MockList } }) vi.mock('../../explore/try-app', () => ({ default: ({ onCreate, onClose }: { onCreate: () => void; onClose: () => void }) => (
), })) vi.mock('../../explore/create-app-modal', () => ({ default: ({ show, onConfirm, onHide, }: { show: boolean onConfirm: (payload: Record) => Promise onHide: () => void }) => show ? (
) : null, })) vi.mock('../../app/create-from-dsl-modal/dsl-confirm-modal', () => ({ default: ({ onConfirm, onCancel }: { onConfirm: () => void; onCancel: () => void }) => (
), })) vi.mock('../import-from-marketplace-template-modal', () => ({ default: ({ templateId, onClose, onConfirm, }: { templateId: string onClose: () => void onConfirm: (dsl: string) => void }) => (
{templateId}
), })) vi.mock('@/service/explore', () => ({ fetchAppDetail: vi.fn(), })) vi.mock('@/utils/create-app-tracking', () => ({ trackCreateApp: (...args: unknown[]) => mockTrackCreateApp(...args), })) describe('Apps', () => { const createQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false, }, }, }) const renderWithClient = (ui: React.ReactElement) => { const queryClient = createQueryClient() const wrapper = ({ children }: { children: ReactNode }) => ( {children} ) return { queryClient, ...render(ui, { wrapper }), } } beforeEach(() => { vi.clearAllMocks() documentTitleCalls = [] educationInitCalls = 0 mockWorkspacePermissionKeys = ['app.create_and_management'] mockSearchParams = new URLSearchParams() mockReplace.mockClear() mockFetchAppDetail.mockResolvedValue({ id: 'template-1', name: 'Sample App', icon: '🤖', icon_background: '#fff', mode: AppModeEnum.CHAT, export_data: 'yaml-content', }) }) describe('Integration', () => { it('should track template preview creation after a successful import', async () => { mockHandleImportDSL.mockImplementation( async ( _payload: unknown, options: { onSuccess?: (payload: { app_mode: AppModeEnum }) => void }, ) => { options.onSuccess?.({ app_mode: AppModeEnum.CHAT }) }, ) renderWithClient() fireEvent.click(screen.getByTestId('open-preview')) fireEvent.click(await screen.findByTestId('try-app-create')) fireEvent.click(await screen.findByTestId('confirm-create')) await waitFor(() => { expect(mockFetchAppDetail).toHaveBeenCalledWith('template-1') expect(mockTrackCreateApp).toHaveBeenCalledWith({ source: 'studio_template_preview', appMode: AppModeEnum.CHAT, templateId: 'template-1', }) }) }) it('should open the template preview from Learn Dify', async () => { const user = userEvent.setup() renderWithClient() await user.click(screen.getByRole('button', { name: 'Preview Learn Dify template' })) expect(await screen.findByTestId('try-app-panel')).toBeInTheDocument() }) it('should open the create modal from Learn Dify', async () => { const user = userEvent.setup() renderWithClient() await user.click(screen.getByRole('button', { name: 'Create Learn Dify template' })) expect(await screen.findByTestId('create-app-modal')).toBeInTheDocument() }) it('should track template preview creation after confirming a pending import', async () => { mockHandleImportDSL.mockImplementation( async (_payload: unknown, options: { onPending?: () => void }) => { options.onPending?.() }, ) mockHandleImportDSLConfirm.mockImplementation( async (options: { onSuccess?: (payload: { app_mode: AppModeEnum }) => void }) => { options.onSuccess?.({ app_mode: AppModeEnum.WORKFLOW }) }, ) renderWithClient() fireEvent.click(screen.getByTestId('open-preview')) fireEvent.click(await screen.findByTestId('try-app-create')) fireEvent.click(await screen.findByTestId('confirm-create')) fireEvent.click(await screen.findByTestId('confirm-dsl')) await waitFor(() => { expect(mockHandleImportDSLConfirm).toHaveBeenCalledTimes(1) expect(mockTrackCreateApp).toHaveBeenCalledWith({ source: 'studio_template_preview', appMode: AppModeEnum.WORKFLOW, templateId: 'template-1', }) }) }) it('should close the dsl confirm modal when the pending import is canceled', async () => { mockHandleImportDSL.mockImplementation( async (_payload: unknown, options: { onPending?: () => void }) => { options.onPending?.() }, ) renderWithClient() fireEvent.click(screen.getByTestId('open-preview')) fireEvent.click(await screen.findByTestId('try-app-create')) fireEvent.click(await screen.findByTestId('confirm-create')) fireEvent.click(await screen.findByTestId('cancel-dsl')) await waitFor(() => { expect(screen.queryByTestId('dsl-confirm-modal')).not.toBeInTheDocument() }) expect(mockTrackCreateApp).not.toHaveBeenCalled() }) it('should hide the create modal without tracking when the modal closes', async () => { renderWithClient() fireEvent.click(screen.getByTestId('open-preview')) fireEvent.click(await screen.findByTestId('try-app-create')) fireEvent.click(await screen.findByTestId('hide-create')) await waitFor(() => { expect(screen.queryByTestId('create-app-modal')).not.toBeInTheDocument() }) expect(mockTrackCreateApp).not.toHaveBeenCalled() }) }) describe('Marketplace Template', () => { it('should render the template modal when template-id is in search params', async () => { mockSearchParams = new URLSearchParams('template-id=tpl-42') renderWithClient() expect(await screen.findByTestId('marketplace-template-modal')).toBeInTheDocument() expect(screen.getByTestId('template-id')).toHaveTextContent('tpl-42') }) it('should not render the template modal without app.create_and_management permission', () => { mockWorkspacePermissionKeys = [] mockSearchParams = new URLSearchParams('template-id=tpl-42') renderWithClient() expect(screen.queryByTestId('marketplace-template-modal')).not.toBeInTheDocument() }) it('should not render the template modal when no template-id is present', () => { renderWithClient() expect(screen.queryByTestId('marketplace-template-modal')).not.toBeInTheDocument() }) it('should close the template modal and remove template-id from URL', async () => { mockSearchParams = new URLSearchParams('template-id=tpl-42') renderWithClient() fireEvent.click(await screen.findByTestId('close-template')) expect(mockReplace).toHaveBeenCalledTimes(1) const replaceArg = mockReplace.mock.calls[0]![0] as string expect(replaceArg).not.toContain('template-id') }) it('should import DSL from marketplace template on confirm', async () => { mockHandleImportDSL.mockImplementation( async ( _payload: unknown, options: { onSuccess?: (payload: { app_mode: AppModeEnum }) => void }, ) => { options.onSuccess?.({ app_mode: AppModeEnum.CHAT }) }, ) mockSearchParams = new URLSearchParams('template-id=tpl-42') renderWithClient() fireEvent.click(await screen.findByTestId('confirm-template')) await waitFor(() => { expect(mockHandleImportDSL).toHaveBeenCalledWith( { mode: 'yaml-content', yaml_content: 'yaml-dsl-content' }, expect.objectContaining({ onSuccess: expect.any(Function) }), ) expect(mockTrackCreateApp).toHaveBeenCalledWith({ source: 'external', appMode: AppModeEnum.CHAT, templateId: 'tpl-42', }) expect(mockReplace).toHaveBeenCalled() }) }) it('should not open create modal from template preview without app.create_and_management permission', async () => { mockWorkspacePermissionKeys = [] renderWithClient() fireEvent.click(screen.getByTestId('open-preview')) fireEvent.click(await screen.findByTestId('try-app-create')) expect(screen.queryByTestId('create-app-modal')).not.toBeInTheDocument() }) it('should track marketplace template creation after confirming a pending import', async () => { mockHandleImportDSL.mockImplementation( async (_payload: unknown, options: { onPending?: () => void }) => { options.onPending?.() }, ) mockHandleImportDSLConfirm.mockImplementation( async (options: { onSuccess?: (payload: { app_mode: AppModeEnum }) => void }) => { options.onSuccess?.({ app_mode: AppModeEnum.WORKFLOW }) }, ) mockSearchParams = new URLSearchParams('template-id=tpl-42') renderWithClient() fireEvent.click(await screen.findByTestId('confirm-template')) await waitFor(() => { expect(screen.getByTestId('dsl-confirm-modal')).toBeInTheDocument() expect(mockReplace).toHaveBeenCalled() }) fireEvent.click(screen.getByTestId('confirm-dsl')) await waitFor(() => { expect(mockTrackCreateApp).toHaveBeenCalledWith({ source: 'external', appMode: AppModeEnum.WORKFLOW, templateId: 'tpl-42', }) }) }) }) })