test: fix flaky CI failures in node-control and firecrawl specs

This commit is contained in:
CodingOnStar
2026-03-16 16:32:07 +08:00
parent 8b62b99d9d
commit 548b20f70b
3 changed files with 113 additions and 25 deletions

View File

@ -1,5 +1,5 @@
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vitest'
@ -55,6 +55,21 @@ const createMockCrawlResultItem = (overrides: Partial<CrawlResultItem> = {}): Cr
...overrides,
})
const createDeferred = <T,>() => {
let resolve!: (value: T | PromiseLike<T>) => void
let reject!: (reason?: unknown) => void
const promise = new Promise<T>((res, rej) => {
resolve = res
reject = rej
})
return {
promise,
resolve,
reject,
}
}
// FireCrawl Component Tests
describe('FireCrawl', () => {
@ -217,7 +232,7 @@ describe('FireCrawl', () => {
await user.click(runButton)
await waitFor(() => {
expect(mockCreateFirecrawlTask).toHaveBeenCalled()
expect(mockOnCheckedCrawlResultChange).toHaveBeenCalledWith([])
})
})
@ -241,7 +256,7 @@ describe('FireCrawl', () => {
await user.click(runButton)
await waitFor(() => {
expect(mockCreateFirecrawlTask).toHaveBeenCalled()
expect(mockOnCheckedCrawlResultChange).toHaveBeenCalledWith([])
})
})
})
@ -277,6 +292,10 @@ describe('FireCrawl', () => {
}),
})
})
await waitFor(() => {
expect(mockOnCheckedCrawlResultChange).toHaveBeenCalledWith([])
})
})
it('should call onJobIdChange with job_id from API response', async () => {
@ -301,6 +320,10 @@ describe('FireCrawl', () => {
await waitFor(() => {
expect(mockOnJobIdChange).toHaveBeenCalledWith('my-job-123')
})
await waitFor(() => {
expect(mockOnCheckedCrawlResultChange).toHaveBeenCalledWith([])
})
})
it('should remove empty max_depth from crawlOptions before sending to API', async () => {
@ -334,11 +357,23 @@ describe('FireCrawl', () => {
}),
})
})
await waitFor(() => {
expect(mockOnCheckedCrawlResultChange).toHaveBeenCalledWith([])
})
})
it('should show loading state while running', async () => {
const user = userEvent.setup()
mockCreateFirecrawlTask.mockImplementation(() => new Promise(() => {})) // Never resolves
const createTaskDeferred = createDeferred<{ job_id: string }>()
mockCreateFirecrawlTask.mockImplementation(() => createTaskDeferred.promise)
mockCheckFirecrawlTaskStatus.mockResolvedValueOnce({
status: 'completed',
data: [],
total: 0,
current: 0,
time_consuming: 1,
})
render(<FireCrawl {...defaultProps} />)
@ -352,6 +387,14 @@ describe('FireCrawl', () => {
await waitFor(() => {
expect(runButton).not.toHaveTextContent(/run/i)
})
await act(async () => {
createTaskDeferred.resolve({ job_id: 'test-job-id' })
})
await waitFor(() => {
expect(mockOnCheckedCrawlResultChange).toHaveBeenCalledWith([])
})
})
})
@ -656,7 +699,7 @@ describe('FireCrawl', () => {
await waitFor(() => {
// Total should be capped to limit (5)
expect(mockCheckFirecrawlTaskStatus).toHaveBeenCalled()
expect(mockOnCheckedCrawlResultChange).toHaveBeenCalledWith([])
})
})
})

View File

@ -2,7 +2,7 @@
import type { FC } from 'react'
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
import * as React from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Toast from '@/app/components/base/toast'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
@ -35,6 +35,22 @@ enum Step {
finished = 'finished',
}
type CrawlState = {
current: number
total: number
data: CrawlResultItem[]
time_consuming: number | string
}
type CrawlFinishedResult = {
isCancelled?: boolean
isError: boolean
errorMessage?: string
data: Partial<CrawlState> & {
data: CrawlResultItem[]
}
}
const FireCrawl: FC<Props> = ({
onPreview,
checkedCrawlResult,
@ -46,10 +62,16 @@ const FireCrawl: FC<Props> = ({
const { t } = useTranslation()
const [step, setStep] = useState<Step>(Step.init)
const [controlFoldOptions, setControlFoldOptions] = useState<number>(0)
const isMountedRef = useRef(true)
useEffect(() => {
if (step !== Step.init)
setControlFoldOptions(Date.now())
}, [step])
useEffect(() => {
return () => {
isMountedRef.current = false
}
}, [])
const setShowAccountSettingModal = useModalContextSelector(s => s.setShowAccountSettingModal)
const handleSetting = useCallback(() => {
setShowAccountSettingModal({
@ -85,16 +107,19 @@ const FireCrawl: FC<Props> = ({
const isInit = step === Step.init
const isCrawlFinished = step === Step.finished
const isRunning = step === Step.running
const [crawlResult, setCrawlResult] = useState<{
current: number
total: number
data: CrawlResultItem[]
time_consuming: number | string
} | undefined>(undefined)
const [crawlResult, setCrawlResult] = useState<CrawlState | undefined>(undefined)
const [crawlErrorMessage, setCrawlErrorMessage] = useState('')
const showError = isCrawlFinished && crawlErrorMessage
const waitForCrawlFinished = useCallback(async (jobId: string) => {
const waitForCrawlFinished = useCallback(async (jobId: string): Promise<CrawlFinishedResult> => {
const cancelledResult: CrawlFinishedResult = {
isCancelled: true,
isError: false,
data: {
data: [],
},
}
try {
const res = await checkFirecrawlTaskStatus(jobId) as any
if (res.status === 'completed') {
@ -104,7 +129,7 @@ const FireCrawl: FC<Props> = ({
...res,
total: Math.min(res.total, Number.parseFloat(crawlOptions.limit as string)),
},
}
} satisfies CrawlFinishedResult
}
if (res.status === 'error' || !res.status) {
// can't get the error message from the firecrawl api
@ -114,12 +139,14 @@ const FireCrawl: FC<Props> = ({
data: {
data: [],
},
}
} satisfies CrawlFinishedResult
}
res.data = res.data.map((item: any) => ({
...item,
content: item.markdown,
}))
if (!isMountedRef.current)
return cancelledResult
// update the progress
setCrawlResult({
...res,
@ -127,17 +154,21 @@ const FireCrawl: FC<Props> = ({
})
onCheckedCrawlResultChange(res.data || []) // default select the crawl result
await sleep(2500)
if (!isMountedRef.current)
return cancelledResult
return await waitForCrawlFinished(jobId)
}
catch (e: any) {
const errorBody = await e.json()
if (!isMountedRef.current)
return cancelledResult
const errorBody = typeof e?.json === 'function' ? await e.json() : undefined
return {
isError: true,
errorMessage: errorBody.message,
errorMessage: errorBody?.message,
data: {
data: [],
},
}
} satisfies CrawlFinishedResult
}
}, [crawlOptions.limit, onCheckedCrawlResultChange])
@ -162,24 +193,31 @@ const FireCrawl: FC<Props> = ({
url,
options: passToServerCrawlOptions,
}) as any
if (!isMountedRef.current)
return
const jobId = res.job_id
onJobIdChange(jobId)
const { isError, data, errorMessage } = await waitForCrawlFinished(jobId)
const { isCancelled, isError, data, errorMessage } = await waitForCrawlFinished(jobId)
if (isCancelled || !isMountedRef.current)
return
if (isError) {
setCrawlErrorMessage(errorMessage || t(`${I18N_PREFIX}.unknownError`, { ns: 'datasetCreation' }))
}
else {
setCrawlResult(data)
setCrawlResult(data as CrawlState)
onCheckedCrawlResultChange(data.data || []) // default select the crawl result
setCrawlErrorMessage('')
}
}
catch (e) {
if (!isMountedRef.current)
return
setCrawlErrorMessage(t(`${I18N_PREFIX}.unknownError`, { ns: 'datasetCreation' })!)
console.log(e)
}
finally {
setStep(Step.finished)
if (isMountedRef.current)
setStep(Step.finished)
}
}, [checkValid, crawlOptions, onJobIdChange, t, waitForCrawlFinished, onCheckedCrawlResultChange])

View File

@ -3,10 +3,17 @@ import { fireEvent, render, screen } from '@testing-library/react'
import { BlockEnum, NodeRunningStatus } from '../../../types'
import NodeControl from './node-control'
const mockHandleNodeSelect = vi.fn()
const mockSetInitShowLastRunTab = vi.fn()
const mockSetPendingSingleRun = vi.fn()
const mockCanRunBySingle = vi.fn(() => true)
const {
mockHandleNodeSelect,
mockSetInitShowLastRunTab,
mockSetPendingSingleRun,
mockCanRunBySingle,
} = vi.hoisted(() => ({
mockHandleNodeSelect: vi.fn(),
mockSetInitShowLastRunTab: vi.fn(),
mockSetPendingSingleRun: vi.fn(),
mockCanRunBySingle: vi.fn(() => true),
}))
vi.mock('react-i18next', () => ({
useTranslation: () => ({