mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 11:04:27 +08:00
fix(web): gate console atom consumers on bootstrap (#38962)
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
parent
7d20b8cc1b
commit
0f0fb1ca3d
113
web/app/(commonLayout)/__tests__/console-bootstrap-gate.spec.tsx
Normal file
113
web/app/(commonLayout)/__tests__/console-bootstrap-gate.spec.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { act, render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { ConsoleBootstrapGate } from '../console-bootstrap-gate'
|
||||||
|
|
||||||
|
const profileQueryKey = ['console', 'account', 'profile', 'get']
|
||||||
|
const systemFeaturesQueryKey = ['console', 'system-features', 'get']
|
||||||
|
|
||||||
|
type Deferred<T> = {
|
||||||
|
promise: Promise<T>
|
||||||
|
resolve: (value: T) => void
|
||||||
|
reject: (reason?: unknown) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const createDeferred = <T,>(): Deferred<T> => {
|
||||||
|
let resolve!: (value: T) => void
|
||||||
|
let reject!: (reason?: unknown) => void
|
||||||
|
const promise = new Promise<T>((nextResolve, nextReject) => {
|
||||||
|
resolve = nextResolve
|
||||||
|
reject = nextReject
|
||||||
|
})
|
||||||
|
|
||||||
|
return { promise, resolve, reject }
|
||||||
|
}
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
profileQuery: undefined as Deferred<{ id: string }> | undefined,
|
||||||
|
systemFeaturesQuery: undefined as Deferred<{ branding: { enabled: boolean } }> | undefined,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/features/account-profile/client', () => ({
|
||||||
|
userProfileQueryOptions: () => ({
|
||||||
|
queryKey: profileQueryKey,
|
||||||
|
queryFn: () => mocks.profileQuery!.promise,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/features/system-features/client', () => ({
|
||||||
|
systemFeaturesQueryOptions: () => ({
|
||||||
|
queryKey: systemFeaturesQueryKey,
|
||||||
|
queryFn: () => mocks.systemFeaturesQuery!.promise,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
function createQueryClient() {
|
||||||
|
return new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: { retry: false },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGate(children: ReactNode, queryClient = createQueryClient()) {
|
||||||
|
render(
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<ConsoleBootstrapGate>{children}</ConsoleBootstrapGate>
|
||||||
|
</QueryClientProvider>,
|
||||||
|
)
|
||||||
|
|
||||||
|
return queryClient
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ConsoleBootstrapGate', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.profileQuery = createDeferred()
|
||||||
|
mocks.systemFeaturesQuery = createDeferred()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('waits for profile and system features before mounting atom consumers', async () => {
|
||||||
|
renderGate(<div>Console shell</div>)
|
||||||
|
|
||||||
|
expect(screen.queryByText('Console shell')).not.toBeInTheDocument()
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
mocks.profileQuery!.resolve({ id: 'user-1' })
|
||||||
|
})
|
||||||
|
expect(screen.queryByText('Console shell')).not.toBeInTheDocument()
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
mocks.systemFeaturesQuery!.resolve({ branding: { enabled: false } })
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(await screen.findByText('Console shell')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps atom consumers mounted when a cached profile background refetch fails', async () => {
|
||||||
|
const queryClient = createQueryClient()
|
||||||
|
queryClient.setQueryData(profileQueryKey, { id: 'user-1' }, { updatedAt: 1 })
|
||||||
|
queryClient.setQueryData(
|
||||||
|
systemFeaturesQueryKey,
|
||||||
|
{ branding: { enabled: false } },
|
||||||
|
{ updatedAt: 1 },
|
||||||
|
)
|
||||||
|
|
||||||
|
renderGate(<div>Console shell</div>, queryClient)
|
||||||
|
|
||||||
|
expect(screen.getByText('Console shell')).toBeInTheDocument()
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(queryClient.getQueryState(profileQueryKey)?.fetchStatus).toBe('fetching')
|
||||||
|
})
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
mocks.profileQuery!.reject(new Error('profile refetch failed'))
|
||||||
|
mocks.systemFeaturesQuery!.resolve({ branding: { enabled: false } })
|
||||||
|
})
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(queryClient.getQueryState(profileQueryKey)?.status).toBe('error')
|
||||||
|
})
|
||||||
|
expect(screen.getByText('Console shell')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
14
web/app/(commonLayout)/console-bootstrap-gate.tsx
Normal file
14
web/app/(commonLayout)/console-bootstrap-gate.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { useSuspenseQueries } from '@tanstack/react-query'
|
||||||
|
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||||
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||||
|
|
||||||
|
export function ConsoleBootstrapGate({ children }: { children: ReactNode }) {
|
||||||
|
useSuspenseQueries({
|
||||||
|
queries: [userProfileQueryOptions(), systemFeaturesQueryOptions()],
|
||||||
|
})
|
||||||
|
|
||||||
|
return children
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import { OAuthRegistrationAnalytics } from '@/app/components/oauth-registration-
|
|||||||
import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
|
import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
|
||||||
import { ModalContextProvider } from '@/context/modal-context-provider'
|
import { ModalContextProvider } from '@/context/modal-context-provider'
|
||||||
import { ProviderContextProvider } from '@/context/provider-context-provider'
|
import { ProviderContextProvider } from '@/context/provider-context-provider'
|
||||||
|
import { ConsoleBootstrapGate } from './console-bootstrap-gate'
|
||||||
import { ExternalServiceSync } from './external-service-sync'
|
import { ExternalServiceSync } from './external-service-sync'
|
||||||
import { CommonLayoutHydrationBoundary } from './hydration-boundary'
|
import { CommonLayoutHydrationBoundary } from './hydration-boundary'
|
||||||
|
|
||||||
@ -17,8 +18,10 @@ export async function ConsoleRuntimeProviders({ children }: { children: ReactNod
|
|||||||
<OAuthRegistrationAnalytics />
|
<OAuthRegistrationAnalytics />
|
||||||
<EducationVerifyActionRecorder />
|
<EducationVerifyActionRecorder />
|
||||||
<CommonLayoutHydrationBoundary>
|
<CommonLayoutHydrationBoundary>
|
||||||
<ExternalServiceSync />
|
<ConsoleBootstrapGate>
|
||||||
{children}
|
<ExternalServiceSync />
|
||||||
|
{children}
|
||||||
|
</ConsoleBootstrapGate>
|
||||||
</CommonLayoutHydrationBoundary>
|
</CommonLayoutHydrationBoundary>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user