diff --git a/web/app/components/app-initializer.tsx b/web/app/components/app-initializer.tsx
index 3410ecbe9a..dfbac5d743 100644
--- a/web/app/components/app-initializer.tsx
+++ b/web/app/components/app-initializer.tsx
@@ -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) {
diff --git a/web/app/components/app/create-app-dialog/app-card/index.tsx b/web/app/components/app/create-app-dialog/app-card/index.tsx
index 15cfbd5411..e203edfc8c 100644
--- a/web/app/components/app/create-app-dialog/app-card/index.tsx
+++ b/web/app/components/app/create-app-dialog/app-card/index.tsx
@@ -62,19 +62,19 @@ const AppCard = ({
{app.description}
- {canCreate && (
+ {(canCreate || isTrialApp) && (
-
-
- {isTrialApp && (
-
)}
diff --git a/web/app/components/explore/app-card/index.tsx b/web/app/components/explore/app-card/index.tsx
index 15152e0695..827c5c3a23 100644
--- a/web/app/components/explore/app-card/index.tsx
+++ b/web/app/components/explore/app-card/index.tsx
@@ -74,11 +74,15 @@ const AppCard = ({
{isExplore && (canCreate || isTrialApp) && (
-
-
onCreate()}>
-
- {t('appCard.addToWorkspace', { ns: 'explore' })}
-
+
+ {
+ canCreate && (
+
onCreate()}>
+
+ {t('appCard.addToWorkspace', { ns: 'explore' })}
+
+ )
+ }
{t('appCard.try', { ns: 'explore' })}
diff --git a/web/app/components/explore/try-app/index.spec.tsx b/web/app/components/explore/try-app/index.spec.tsx
index 49d35ce58d..dc057b4d9f 100644
--- a/web/app/components/explore/try-app/index.spec.tsx
+++ b/web/app/components/explore/try-app/index.spec.tsx
@@ -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()
+ const actual = await importOriginal() as object
return {
...actual,
IS_CLOUD_EDITION: true,
diff --git a/web/app/components/explore/try-app/tab.spec.tsx b/web/app/components/explore/try-app/tab.spec.tsx
index 93b291b2c0..af64a93f43 100644
--- a/web/app/components/explore/try-app/tab.spec.tsx
+++ b/web/app/components/explore/try-app/tab.spec.tsx
@@ -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(() => {
diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.spec.tsx
index 543d3deebc..9155fa15be 100644
--- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.spec.tsx
+++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.spec.tsx
@@ -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' } })
diff --git a/web/app/components/rag-pipeline/components/update-dsl-modal.spec.tsx b/web/app/components/rag-pipeline/components/update-dsl-modal.spec.tsx
index b96d3dfb1f..23b92f3e15 100644
--- a/web/app/components/rag-pipeline/components/update-dsl-modal.spec.tsx
+++ b/web/app/components/rag-pipeline/components/update-dsl-modal.spec.tsx
@@ -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 () => {
diff --git a/web/app/components/rag-pipeline/hooks/use-DSL.spec.ts b/web/app/components/rag-pipeline/hooks/use-DSL.spec.ts
index 3d0a448462..c6e3d261c0 100644
--- a/web/app/components/rag-pipeline/hooks/use-DSL.spec.ts
+++ b/web/app/components/rag-pipeline/hooks/use-DSL.spec.ts
@@ -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 () => {
diff --git a/web/app/components/tools/edit-custom-collection-modal/index.spec.tsx b/web/app/components/tools/edit-custom-collection-modal/index.spec.tsx
index b993b6a779..97fc03175d 100644
--- a/web/app/components/tools/edit-custom-collection-modal/index.spec.tsx
+++ b/web/app/components/tools/edit-custom-collection-modal/index.spec.tsx
@@ -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'))