mirror of
https://github.com/langgenius/dify.git
synced 2026-03-09 17:36:44 +08:00
Merge remote-tracking branch 'origin/main' into refactor/datasets-components
This commit is contained in:
@ -3,7 +3,7 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import Cookies from 'js-cookie'
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||
import { parseAsString, useQueryState } from 'nuqs'
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION,
|
||||
@ -28,7 +28,7 @@ export const AppInitializer = ({
|
||||
const [init, setInit] = useState(false)
|
||||
const [oauthNewUser, setOauthNewUser] = useQueryState(
|
||||
'oauth_new_user',
|
||||
parseAsString.withOptions({ history: 'replace' }),
|
||||
parseAsBoolean.withOptions({ history: 'replace' }),
|
||||
)
|
||||
|
||||
const isSetupFinished = useCallback(async () => {
|
||||
@ -46,7 +46,7 @@ export const AppInitializer = ({
|
||||
(async () => {
|
||||
const action = searchParams.get('action')
|
||||
|
||||
if (oauthNewUser === 'true') {
|
||||
if (oauthNewUser) {
|
||||
let utmInfo = null
|
||||
const utmInfoStr = Cookies.get('utm_info')
|
||||
if (utmInfoStr) {
|
||||
|
||||
@ -62,19 +62,19 @@ const AppCard = ({
|
||||
{app.description}
|
||||
</div>
|
||||
</div>
|
||||
{canCreate && (
|
||||
{(canCreate || isTrialApp) && (
|
||||
<div className={cn('absolute bottom-0 left-0 right-0 hidden bg-gradient-to-t from-components-panel-gradient-2 from-[60.27%] to-transparent p-4 pt-8 group-hover:flex')}>
|
||||
<div className={cn('grid h-8 w-full grid-cols-1 items-center space-x-2', isTrialApp && 'grid-cols-2')}>
|
||||
<Button variant="primary" onClick={() => onCreate()}>
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
<span className="text-xs">{t('newApp.useTemplate', { ns: 'app' })}</span>
|
||||
</Button>
|
||||
{isTrialApp && (
|
||||
<Button onClick={showTryAPPPanel(app.app_id)}>
|
||||
<RiInformation2Line className="mr-1 size-4" />
|
||||
<span>{t('appCard.try', { ns: 'explore' })}</span>
|
||||
<div className={cn('grid h-8 w-full grid-cols-1 items-center space-x-2', canCreate && 'grid-cols-2')}>
|
||||
{canCreate && (
|
||||
<Button variant="primary" onClick={() => onCreate()}>
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
<span className="text-xs">{t('newApp.useTemplate', { ns: 'app' })}</span>
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={showTryAPPPanel(app.app_id)}>
|
||||
<RiInformation2Line className="mr-1 size-4" />
|
||||
<span>{t('appCard.try', { ns: 'explore' })}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -74,11 +74,15 @@ const AppCard = ({
|
||||
</div>
|
||||
{isExplore && (canCreate || isTrialApp) && (
|
||||
<div className={cn('absolute bottom-0 left-0 right-0 hidden bg-gradient-to-t from-components-panel-gradient-2 from-[60.27%] to-transparent p-4 pt-8 group-hover:flex')}>
|
||||
<div className={cn('grid h-8 w-full grid-cols-2 space-x-2')}>
|
||||
<Button variant="primary" className="h-7" onClick={() => onCreate()}>
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
<span className="text-xs">{t('appCard.addToWorkspace', { ns: 'explore' })}</span>
|
||||
</Button>
|
||||
<div className={cn('grid h-8 w-full grid-cols-1 space-x-2', canCreate && 'grid-cols-2')}>
|
||||
{
|
||||
canCreate && (
|
||||
<Button variant="primary" className="h-7" onClick={() => onCreate()}>
|
||||
<PlusIcon className="mr-1 h-4 w-4" />
|
||||
<span className="text-xs">{t('appCard.addToWorkspace', { ns: 'explore' })}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<Button className="h-7" onClick={showTryAPPPanel(app.app_id)}>
|
||||
<RiInformation2Line className="mr-1 size-4" />
|
||||
<span>{t('appCard.try', { ns: 'explore' })}</span>
|
||||
|
||||
@ -16,9 +16,8 @@ vi.mock('react-i18next', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock IS_CLOUD_EDITION to be true so Try tab is shown
|
||||
vi.mock('@/config', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/config')>()
|
||||
const actual = await importOriginal() as object
|
||||
return {
|
||||
...actual,
|
||||
IS_CLOUD_EDITION: true,
|
||||
|
||||
@ -14,10 +14,13 @@ vi.mock('react-i18next', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock IS_CLOUD_EDITION to be true so Try tab is shown
|
||||
vi.mock('@/config', () => ({
|
||||
IS_CLOUD_EDITION: true,
|
||||
}))
|
||||
vi.mock('@/config', async (importOriginal) => {
|
||||
const actual = await importOriginal() as object
|
||||
return {
|
||||
...actual,
|
||||
IS_CLOUD_EDITION: true,
|
||||
}
|
||||
})
|
||||
|
||||
describe('Tab', () => {
|
||||
afterEach(() => {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
// Import after mocks
|
||||
@ -821,6 +821,9 @@ describe('CommonCreateModal', () => {
|
||||
expect(mockCreateBuilder).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Flush pending state updates from createBuilder promise resolution
|
||||
await act(async () => {})
|
||||
|
||||
const input = screen.getByTestId('form-field-webhook_url')
|
||||
fireEvent.change(input, { target: { value: 'https://example.com/webhook' } })
|
||||
|
||||
|
||||
@ -140,13 +140,13 @@ class MockFileReader {
|
||||
onload: ((e: { target: { result: string | null } }) => void) | null = null
|
||||
|
||||
readAsText(_file: File) {
|
||||
// Simulate async file reading
|
||||
setTimeout(() => {
|
||||
// Simulate async file reading using queueMicrotask for more reliable async behavior
|
||||
queueMicrotask(() => {
|
||||
this.result = 'test file content'
|
||||
if (this.onload) {
|
||||
this.onload({ target: { result: this.result } })
|
||||
}
|
||||
}, 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,6 +174,7 @@ describe('UpdateDSLModal', () => {
|
||||
status: DSLImportStatus.COMPLETED,
|
||||
pipeline_id: 'test-pipeline-id',
|
||||
})
|
||||
mockHandleCheckPluginDependencies.mockResolvedValue(undefined)
|
||||
|
||||
// Mock FileReader
|
||||
originalFileReader = globalThis.FileReader
|
||||
@ -472,14 +473,14 @@ describe('UpdateDSLModal', () => {
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
})
|
||||
}, { timeout: 1000 })
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
fireEvent.click(importButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockOnImport).toHaveBeenCalled()
|
||||
})
|
||||
}, { timeout: 1000 })
|
||||
})
|
||||
|
||||
it('should show warning notification on import with warnings', async () => {
|
||||
@ -664,7 +665,7 @@ describe('UpdateDSLModal', () => {
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
})
|
||||
}, { timeout: 1000 })
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
fireEvent.click(importButton)
|
||||
@ -672,7 +673,7 @@ describe('UpdateDSLModal', () => {
|
||||
// Wait for the error modal to be shown after setTimeout
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
|
||||
}, { timeout: 500 })
|
||||
}, { timeout: 1000 })
|
||||
})
|
||||
|
||||
it('should show version info in error modal', async () => {
|
||||
|
||||
@ -33,23 +33,21 @@ vi.mock('@/service/workflow', () => ({
|
||||
fetchWorkflowDraft: (...args: unknown[]) => mockFetchWorkflowDraft(...args),
|
||||
}))
|
||||
|
||||
const mockDownloadBlob = vi.fn()
|
||||
vi.mock('@/utils/download', () => ({
|
||||
downloadBlob: (...args: unknown[]) => mockDownloadBlob(...args),
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock workflow constants
|
||||
vi.mock('@/app/components/workflow/constants', () => ({
|
||||
DSL_EXPORT_CHECK: 'DSL_EXPORT_CHECK',
|
||||
}))
|
||||
|
||||
// Mock downloadBlob utility
|
||||
const mockDownloadBlob = vi.fn()
|
||||
vi.mock('@/utils/download', () => ({
|
||||
downloadBlob: (params: unknown) => mockDownloadBlob(params),
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
@ -93,9 +91,7 @@ describe('useDSL', () => {
|
||||
await result.current.handleExportDSL()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDownloadBlob).toHaveBeenCalled()
|
||||
})
|
||||
expect(mockDownloadBlob).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should set correct download filename', async () => {
|
||||
@ -105,27 +101,25 @@ describe('useDSL', () => {
|
||||
await result.current.handleExportDSL()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDownloadBlob).toHaveBeenCalledWith({
|
||||
data: expect.any(Blob),
|
||||
expect(mockDownloadBlob).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
fileName: 'Test Knowledge Base.pipeline',
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should call downloadBlob with correct blob data', async () => {
|
||||
it('should pass blob data to downloadBlob', async () => {
|
||||
const { result } = renderHook(() => useDSL())
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleExportDSL()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDownloadBlob).toHaveBeenCalled()
|
||||
const callArgs = mockDownloadBlob.mock.calls[0][0]
|
||||
expect(callArgs.data).toBeInstanceOf(Blob)
|
||||
expect(callArgs.fileName).toBe('Test Knowledge Base.pipeline')
|
||||
})
|
||||
expect(mockDownloadBlob).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.any(Blob),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle export error', async () => {
|
||||
|
||||
@ -173,10 +173,8 @@ describe('EditCustomCollectionModal', () => {
|
||||
expect(parseParamsSchemaMock).toHaveBeenCalledWith('{}')
|
||||
})
|
||||
|
||||
// Wait for the async useEffect to complete and update schema_type
|
||||
await act(async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
})
|
||||
// Flush pending state updates from parseParamsSchema promise resolution
|
||||
await act(async () => {})
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('common.operation.save'))
|
||||
|
||||
Reference in New Issue
Block a user