mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 04:36:31 +08:00
Changes before error encountered
Co-authored-by: hyoban <38493346+hyoban@users.noreply.github.com>
This commit is contained in:
parent
3a64db3d60
commit
039b448b90
@ -1541,7 +1541,7 @@ describe('AppSelector', () => {
|
||||
|
||||
it('should manage isLoadingMore state during load more', () => {
|
||||
mockHasNextPage = true
|
||||
mockFetchNextPage.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100)))
|
||||
mockFetchNextPage.mockResolvedValue(undefined)
|
||||
|
||||
renderWithQueryClient(<AppSelector {...defaultProps} />)
|
||||
|
||||
@ -1769,7 +1769,10 @@ describe('AppSelector', () => {
|
||||
it('should not call fetchNextPage when isLoadingMore is true', async () => {
|
||||
mockHasNextPage = true
|
||||
mockIsFetchingNextPage = false
|
||||
mockFetchNextPage.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 1000)))
|
||||
let resolveFetchNextPage: (() => void) | undefined
|
||||
mockFetchNextPage.mockImplementation(() => new Promise<void>((resolve) => {
|
||||
resolveFetchNextPage = resolve
|
||||
}))
|
||||
|
||||
renderWithQueryClient(<AppSelector {...defaultProps} />)
|
||||
|
||||
@ -1780,8 +1783,13 @@ describe('AppSelector', () => {
|
||||
|
||||
// Trigger intersection - this starts loading
|
||||
triggerIntersection([{ isIntersecting: true } as IntersectionObserverEntry])
|
||||
triggerIntersection([{ isIntersecting: true } as IntersectionObserverEntry])
|
||||
|
||||
expect(mockFetchNextPage).toHaveBeenCalledTimes(1)
|
||||
|
||||
await act(async () => {
|
||||
resolveFetchNextPage?.()
|
||||
})
|
||||
})
|
||||
|
||||
it('should skip handleLoadMore when isFetchingNextPage is true', async () => {
|
||||
@ -1825,8 +1833,10 @@ describe('AppSelector', () => {
|
||||
it('should return early from handleLoadMore when isLoadingMore is true', async () => {
|
||||
mockHasNextPage = true
|
||||
mockIsFetchingNextPage = false
|
||||
// Make fetchNextPage slow to keep isLoadingMore true
|
||||
mockFetchNextPage.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 5000)))
|
||||
let resolveFetchNextPage: (() => void) | undefined
|
||||
mockFetchNextPage.mockImplementation(() => new Promise<void>((resolve) => {
|
||||
resolveFetchNextPage = resolve
|
||||
}))
|
||||
|
||||
renderWithQueryClient(<AppSelector {...defaultProps} />)
|
||||
|
||||
@ -1843,6 +1853,10 @@ describe('AppSelector', () => {
|
||||
|
||||
// Still only 1 call because isLoadingMore blocks it
|
||||
expect(mockFetchNextPage).toHaveBeenCalledTimes(1)
|
||||
|
||||
await act(async () => {
|
||||
resolveFetchNextPage?.()
|
||||
})
|
||||
})
|
||||
|
||||
it('should reset isLoadingMore via setTimeout after fetchNextPage resolves', async () => {
|
||||
|
||||
@ -54,6 +54,7 @@ const AppPicker: FC<Props> = ({
|
||||
const observerTarget = useRef<HTMLDivElement>(null)
|
||||
const observerRef = useRef<IntersectionObserver | null>(null)
|
||||
const loadingRef = useRef(false)
|
||||
const loadingResetTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
const handleIntersection = useCallback((entries: IntersectionObserverEntry[]) => {
|
||||
const target = entries[0]
|
||||
@ -63,8 +64,11 @@ const AppPicker: FC<Props> = ({
|
||||
loadingRef.current = true
|
||||
onLoadMore()
|
||||
// Reset loading state
|
||||
setTimeout(() => {
|
||||
if (loadingResetTimeoutRef.current)
|
||||
clearTimeout(loadingResetTimeoutRef.current)
|
||||
loadingResetTimeoutRef.current = setTimeout(() => {
|
||||
loadingRef.current = false
|
||||
loadingResetTimeoutRef.current = null
|
||||
}, 500)
|
||||
}, [hasMore, isLoading, onLoadMore])
|
||||
|
||||
@ -116,6 +120,10 @@ const AppPicker: FC<Props> = ({
|
||||
observerRef.current.disconnect()
|
||||
observerRef.current = null
|
||||
}
|
||||
if (loadingResetTimeoutRef.current) {
|
||||
clearTimeout(loadingResetTimeoutRef.current)
|
||||
loadingResetTimeoutRef.current = null
|
||||
}
|
||||
mutationObserver?.disconnect()
|
||||
}
|
||||
}, [isShow, handleIntersection])
|
||||
|
||||
@ -6,7 +6,7 @@ import type {
|
||||
import type { FC } from 'react'
|
||||
import type { App } from '@/types/app'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
@ -50,6 +50,7 @@ const AppSelector: FC<Props> = ({
|
||||
const [isShow, onShowChange] = useState(false)
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
||||
const loadingMoreTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
const {
|
||||
data,
|
||||
@ -106,12 +107,24 @@ const AppSelector: FC<Props> = ({
|
||||
}
|
||||
finally {
|
||||
// Add a small delay to ensure state updates are complete
|
||||
setTimeout(() => {
|
||||
if (loadingMoreTimeoutRef.current)
|
||||
clearTimeout(loadingMoreTimeoutRef.current)
|
||||
loadingMoreTimeoutRef.current = setTimeout(() => {
|
||||
setIsLoadingMore(false)
|
||||
loadingMoreTimeoutRef.current = null
|
||||
}, 300)
|
||||
}
|
||||
}, [isLoadingMore, isFetchingNextPage, hasMore, fetchNextPage])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (loadingMoreTimeoutRef.current) {
|
||||
clearTimeout(loadingMoreTimeoutRef.current)
|
||||
loadingMoreTimeoutRef.current = null
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleTriggerClick = () => {
|
||||
if (disabled)
|
||||
return
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
|
||||
|
||||
import Conversion from '../conversion'
|
||||
@ -347,6 +348,47 @@ vi.mock('@/app/components/workflow/dsl-export-confirm-modal', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/modal', () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="modal" role="dialog">{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: {
|
||||
notify: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/app-icon-picker', () => ({
|
||||
default: function MockAppIconPicker({
|
||||
onSelect,
|
||||
onClose,
|
||||
}: {
|
||||
onSelect: (selection: { type: 'emoji' | 'image', icon?: string, background?: string, url?: string }) => void
|
||||
onClose: () => void
|
||||
}) {
|
||||
const [pendingSelection, setPendingSelection] = React.useState<{ type: 'emoji' | 'image', icon?: string, background?: string, url?: string } | null>(null)
|
||||
|
||||
return (
|
||||
<div data-testid="app-icon-picker">
|
||||
<button onClick={() => setPendingSelection({ type: 'emoji', icon: '🤖', background: '#FFEAD5' })}>
|
||||
iconPicker.emojiOption
|
||||
</button>
|
||||
<button onClick={() => setPendingSelection({ type: 'image', url: '/icon.png' })}>
|
||||
iconPicker.image
|
||||
</button>
|
||||
<button onClick={() => pendingSelection && onSelect(pendingSelection)}>
|
||||
iconPicker.ok
|
||||
</button>
|
||||
<button onClick={onClose}>
|
||||
iconPicker.cancel
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
// Silence expected console.error from Dialog/Modal rendering
|
||||
beforeEach(() => {
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
@ -708,10 +750,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
const appIcon = getAppIcon()
|
||||
fireEvent.click(appIcon)
|
||||
|
||||
// Click the first emoji in the grid (search full document since Dialog uses portal)
|
||||
const gridEmojis = document.querySelectorAll('.grid em-emoji')
|
||||
expect(gridEmojis.length).toBeGreaterThan(0)
|
||||
fireEvent.click(gridEmojis[0].parentElement!.parentElement!)
|
||||
fireEvent.click(screen.getByRole('button', { name: /iconPicker\.emojiOption/ }))
|
||||
|
||||
// Click OK to confirm selection
|
||||
fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ }))
|
||||
@ -728,9 +767,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
const appIcon = getAppIcon()
|
||||
fireEvent.click(appIcon)
|
||||
|
||||
// Switch to image tab
|
||||
const imageTab = screen.getByRole('button', { name: /iconPicker\.image/ })
|
||||
fireEvent.click(imageTab)
|
||||
fireEvent.click(screen.getByRole('button', { name: /iconPicker\.image/ }))
|
||||
|
||||
// Picker should still be open
|
||||
expect(screen.getByRole('button', { name: /iconPicker\.ok/ })).toBeInTheDocument()
|
||||
@ -1031,11 +1068,8 @@ describe('Integration Tests', () => {
|
||||
// Open picker and select an emoji
|
||||
const appIcon = getAppIcon()
|
||||
fireEvent.click(appIcon)
|
||||
const gridEmojis = document.querySelectorAll('.grid em-emoji')
|
||||
if (gridEmojis.length > 0) {
|
||||
fireEvent.click(gridEmojis[0].parentElement!.parentElement!)
|
||||
fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ }))
|
||||
}
|
||||
fireEvent.click(screen.getByRole('button', { name: /iconPicker\.emojiOption/ }))
|
||||
fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ }))
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /workflow\.common\.publish/i }))
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user