This commit is contained in:
Jony 2026-08-15 10:46:03 +08:00 committed by GitHub
commit 7e9b37b6a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 66 additions and 1 deletions

View File

@ -22,6 +22,7 @@ const mockOnGraphReadyChange = vi.hoisted(() => vi.fn())
const mockRefreshGraphSynchronously = vi.hoisted(() => vi.fn())
const mockReplaceGraphFromReactFlow = vi.hoisted(() => vi.fn())
const mockCanPersistLocalGraph = vi.hoisted(() => vi.fn())
const mockCanUseLocalDraftFallback = vi.hoisted(() => vi.fn())
const mockIsGraphReloadCurrent = vi.hoisted(() => vi.fn())
const mockRetryGraphReload = vi.hoisted(() => vi.fn())
@ -68,6 +69,7 @@ const collaborationRuntime = vi.hoisted(() => ({
cursors: {} as Record<string, { x: number; y: number; userId: string; timestamp: number }>,
isConnected: false,
isEnabled: false,
connectionError: null as string | null,
}))
const collaborationListeners = vi.hoisted(() => ({
@ -142,6 +144,7 @@ vi.mock('@/app/components/workflow/collaboration/hooks/use-collaboration', () =>
cursors: collaborationRuntime.cursors,
isConnected: collaborationRuntime.isConnected,
isEnabled: collaborationRuntime.isEnabled,
connectionError: collaborationRuntime.connectionError,
}),
}))
@ -187,6 +190,7 @@ vi.mock('@/app/components/workflow/collaboration/core/collaboration-manager', ()
refreshGraphSynchronously: mockRefreshGraphSynchronously,
replaceGraphFromReactFlow: mockReplaceGraphFromReactFlow,
canPersistLocalGraph: mockCanPersistLocalGraph,
canUseLocalDraftFallback: mockCanUseLocalDraftFallback,
isGraphReloadCurrent: mockIsGraphReloadCurrent,
retryGraphReload: mockRetryGraphReload,
getIsLeader: mockGetIsLeader,
@ -436,6 +440,7 @@ describe('WorkflowMain', () => {
collaborationRuntime.cursors = {}
collaborationRuntime.isConnected = false
collaborationRuntime.isEnabled = false
collaborationRuntime.connectionError = null
collaborationListeners.varsAndFeaturesUpdate = null
collaborationListeners.workflowUpdate = null
collaborationListeners.syncRequest = null
@ -445,6 +450,7 @@ describe('WorkflowMain', () => {
hookFns.doSyncWorkflowDraft.mockReset()
mockGetIsLeader.mockReturnValue(true)
mockCanPersistLocalGraph.mockReturnValue(true)
mockCanUseLocalDraftFallback.mockReturnValue(false)
mockIsGraphReloadCurrent.mockReturnValue(true)
mockReplaceGraphFromReactFlow.mockReturnValue(true)
hookFns.doSyncWorkflowDraft.mockResolvedValue({ hash: 'saved-hash', updatedAt: 2 })
@ -590,6 +596,31 @@ describe('WorkflowMain', () => {
expect(screen.queryByRole('status')).not.toBeInTheDocument()
})
it('uses the local draft when the initial collaboration connection fails', () => {
collaborationRuntime.isEnabled = true
collaborationRuntime.connectionError = 'websocket error'
mockCanUseLocalDraftFallback.mockReturnValue(true)
render(<WorkflowMain nodes={[]} edges={[]} viewport={{ x: 0, y: 0, zoom: 1 }} />)
act(() => collaborationListeners.graphReadyChange?.(false))
expect(screen.queryByTestId('collaboration-graph-loading')).not.toBeInTheDocument()
expect(screen.getByTestId('workflow-inner-context')).toBeInTheDocument()
})
it('keeps blocking after an established collaboration session disconnects', () => {
collaborationRuntime.isEnabled = true
collaborationRuntime.connectionError = 'transport close'
mockCanUseLocalDraftFallback.mockReturnValue(false)
render(<WorkflowMain nodes={[]} edges={[]} viewport={{ x: 0, y: 0, zoom: 1 }} />)
act(() => collaborationListeners.graphReadyChange?.(false))
expect(screen.getByRole('status')).toHaveTextContent('workflow.common.syncingData')
})
it('subscribes collaboration listeners and handles sync/workflow update callbacks', async () => {
collaborationRuntime.isEnabled = true
mockFetchWorkflowDraft.mockResolvedValue({

View File

@ -80,6 +80,7 @@ const WorkflowMain = ({ nodes, edges, viewport }: WorkflowMainProps) => {
onlineUsers,
cursors,
isConnected,
connectionError,
isEnabled: isCollaborationEnabled,
} = useCollaboration(appId || '', reactFlowStore)
const myUserId = useMemo(
@ -537,6 +538,7 @@ const WorkflowMain = ({ nodes, edges, viewport }: WorkflowMainProps) => {
<WorkflowChildren />
</WorkflowWithInnerContext>
{isCollaborationEnabled &&
!(connectionError && collaborationManager.canUseLocalDraftFallback()) &&
(collaborationGraphState.appId !== appId || !collaborationGraphState.isReady) && (
<div
data-testid="collaboration-graph-loading"

View File

@ -169,4 +169,26 @@ describe('useCollaboration', () => {
result.current.startCursorTracking({ current: document.createElement('div') })
expect(mockStartTracking).not.toHaveBeenCalled()
})
it('exposes an initial connection error and clears it after reconnecting', async () => {
const { result } = renderHookWithConsoleQuery(() => useCollaboration('app-1'), {
systemFeatures: { enable_collaboration_mode: true },
})
await waitFor(() => {
expect(onStateChangeCallback).not.toBeNull()
})
onStateChangeCallback?.({ isConnected: false, error: 'websocket error' })
await waitFor(() => {
expect(result.current.connectionError).toBe('websocket error')
})
onStateChangeCallback?.({ isConnected: true })
await waitFor(() => {
expect(result.current.connectionError).toBeNull()
})
})
})

View File

@ -13,6 +13,7 @@ import { CursorService } from '../services/cursor-service'
type CollaborationViewState = {
isConnected: boolean
connectionError: string | null
onlineUsers: OnlineUser[]
cursors: Record<string, CursorPosition>
nodePanelPresence: NodePanelPresenceMap
@ -23,6 +24,7 @@ type ReactFlowStore = NonNullable<Parameters<typeof collaborationManager.connect
const initialState: CollaborationViewState = {
isConnected: false,
connectionError: null,
onlineUsers: [],
cursors: {},
nodePanelPresence: {},
@ -76,7 +78,14 @@ export function useCollaboration(appId: string, reactFlowStore?: ReactFlowStore)
if (newState.isConnected === undefined) return
setState((prev) => ({ ...prev, isConnected: newState.isConnected ?? prev.isConnected }))
setState((prev) => ({
...prev,
isConnected: newState.isConnected ?? prev.isConnected,
connectionError:
newState.isConnected === true
? null
: (newState.error ?? newState.disconnectReason ?? prev.connectionError),
}))
},
)
@ -145,6 +154,7 @@ export function useCollaboration(appId: string, reactFlowStore?: ReactFlowStore)
const result = {
isConnected: state.isConnected || false,
connectionError: state.connectionError,
onlineUsers: state.onlineUsers || [],
cursors: state.cursors || {},
nodePanelPresence: state.nodePanelPresence || {},