mirror of
https://github.com/langgenius/dify.git
synced 2026-04-29 06:58:05 +08:00
fix(web): prevent false 404 during installed app refetch
- keep loading state when installed app list is refetching without a matched id - align installed-app tests with TanStack Query isPending semantics - add refetch regression assertions to avoid early 404 fallback
This commit is contained in:
@ -80,17 +80,20 @@ describe('Installed App Flow', () => {
|
||||
}
|
||||
|
||||
type MockOverrides = {
|
||||
installedApps?: { apps?: InstalledAppModel[], isPending?: boolean }
|
||||
accessMode?: { isLoading?: boolean, data?: unknown, error?: unknown }
|
||||
params?: { isLoading?: boolean, data?: unknown, error?: unknown }
|
||||
meta?: { isLoading?: boolean, data?: unknown, error?: unknown }
|
||||
installedApps?: { apps?: InstalledAppModel[], isPending?: boolean, isFetching?: boolean }
|
||||
accessMode?: { isPending?: boolean, data?: unknown, error?: unknown }
|
||||
params?: { isPending?: boolean, data?: unknown, error?: unknown }
|
||||
meta?: { isPending?: boolean, data?: unknown, error?: unknown }
|
||||
userAccess?: { data?: unknown, error?: unknown }
|
||||
}
|
||||
|
||||
const setupDefaultMocks = (app?: InstalledAppModel, overrides: MockOverrides = {}) => {
|
||||
const installedApps = overrides.installedApps?.apps ?? (app ? [app] : [])
|
||||
|
||||
;(useGetInstalledApps as Mock).mockReturnValue({
|
||||
data: { installed_apps: app ? [app] : [] },
|
||||
data: { installed_apps: installedApps },
|
||||
isPending: false,
|
||||
isFetching: false,
|
||||
...overrides.installedApps,
|
||||
})
|
||||
|
||||
@ -105,21 +108,21 @@ describe('Installed App Flow', () => {
|
||||
})
|
||||
|
||||
;(useGetInstalledAppAccessModeByAppId as Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
data: { accessMode: AccessMode.PUBLIC },
|
||||
error: null,
|
||||
...overrides.accessMode,
|
||||
})
|
||||
|
||||
;(useGetInstalledAppParams as Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
data: mockAppParams,
|
||||
error: null,
|
||||
...overrides.params,
|
||||
})
|
||||
|
||||
;(useGetInstalledAppMeta as Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
data: { tool_icons: {} },
|
||||
error: null,
|
||||
...overrides.meta,
|
||||
@ -176,7 +179,7 @@ describe('Installed App Flow', () => {
|
||||
describe('Data Loading Flow', () => {
|
||||
it('should show loading spinner when params are being fetched', () => {
|
||||
const app = createInstalledApp()
|
||||
setupDefaultMocks(app, { params: { isLoading: true, data: null } })
|
||||
setupDefaultMocks(app, { params: { isPending: true, data: null } })
|
||||
|
||||
const { container } = render(<InstalledApp id="installed-app-1" />)
|
||||
|
||||
@ -184,6 +187,17 @@ describe('Installed App Flow', () => {
|
||||
expect(screen.queryByTestId('chat-with-history')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should defer 404 while installed apps are refetching without a match', () => {
|
||||
setupDefaultMocks(undefined, {
|
||||
installedApps: { apps: [], isPending: false, isFetching: true },
|
||||
})
|
||||
|
||||
const { container } = render(<InstalledApp id="nonexistent" />)
|
||||
|
||||
expect(container.querySelector('svg.spin-animation')).toBeInTheDocument()
|
||||
expect(screen.queryByText(/404/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render content when all data is available', () => {
|
||||
const app = createInstalledApp()
|
||||
setupDefaultMocks(app)
|
||||
|
||||
@ -91,10 +91,22 @@ describe('InstalledApp', () => {
|
||||
result: true,
|
||||
}
|
||||
|
||||
const setupMocks = (installedApps: InstalledAppType[] = [mockInstalledApp], isPending = false) => {
|
||||
const setupMocks = (
|
||||
installedApps: InstalledAppType[] = [mockInstalledApp],
|
||||
options: {
|
||||
isPending?: boolean
|
||||
isFetching?: boolean
|
||||
} = {},
|
||||
) => {
|
||||
const {
|
||||
isPending = false,
|
||||
isFetching = false,
|
||||
} = options
|
||||
|
||||
;(useGetInstalledApps as Mock).mockReturnValue({
|
||||
data: { installed_apps: installedApps },
|
||||
isPending,
|
||||
isFetching,
|
||||
})
|
||||
}
|
||||
|
||||
@ -123,19 +135,19 @@ describe('InstalledApp', () => {
|
||||
})
|
||||
|
||||
;(useGetInstalledAppAccessModeByAppId as Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
data: mockWebAppAccessMode,
|
||||
error: null,
|
||||
})
|
||||
|
||||
;(useGetInstalledAppParams as Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
data: mockAppParams,
|
||||
error: null,
|
||||
})
|
||||
|
||||
;(useGetInstalledAppMeta as Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
data: mockAppMeta,
|
||||
error: null,
|
||||
})
|
||||
@ -154,7 +166,7 @@ describe('InstalledApp', () => {
|
||||
|
||||
it('should render loading state when fetching app params', () => {
|
||||
;(useGetInstalledAppParams as Mock).mockReturnValue({
|
||||
isLoading: true,
|
||||
isPending: true,
|
||||
data: null,
|
||||
error: null,
|
||||
})
|
||||
@ -166,7 +178,7 @@ describe('InstalledApp', () => {
|
||||
|
||||
it('should render loading state when fetching app meta', () => {
|
||||
;(useGetInstalledAppMeta as Mock).mockReturnValue({
|
||||
isLoading: true,
|
||||
isPending: true,
|
||||
data: null,
|
||||
error: null,
|
||||
})
|
||||
@ -178,7 +190,7 @@ describe('InstalledApp', () => {
|
||||
|
||||
it('should render loading state when fetching web app access mode', () => {
|
||||
;(useGetInstalledAppAccessModeByAppId as Mock).mockReturnValue({
|
||||
isLoading: true,
|
||||
isPending: true,
|
||||
data: null,
|
||||
error: null,
|
||||
})
|
||||
@ -189,7 +201,7 @@ describe('InstalledApp', () => {
|
||||
})
|
||||
|
||||
it('should render loading state when fetching installed apps', () => {
|
||||
setupMocks([mockInstalledApp], true)
|
||||
setupMocks([mockInstalledApp], { isPending: true })
|
||||
|
||||
const { container } = render(<InstalledApp id="installed-app-123" />)
|
||||
const svg = container.querySelector('svg.spin-animation')
|
||||
@ -208,7 +220,7 @@ describe('InstalledApp', () => {
|
||||
it('should render error when app params fails to load', () => {
|
||||
const error = new Error('Failed to load app params')
|
||||
;(useGetInstalledAppParams as Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
data: null,
|
||||
error,
|
||||
})
|
||||
@ -220,7 +232,7 @@ describe('InstalledApp', () => {
|
||||
it('should render error when app meta fails to load', () => {
|
||||
const error = new Error('Failed to load app meta')
|
||||
;(useGetInstalledAppMeta as Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
data: null,
|
||||
error,
|
||||
})
|
||||
@ -232,7 +244,7 @@ describe('InstalledApp', () => {
|
||||
it('should render error when web app access mode fails to load', () => {
|
||||
const error = new Error('Failed to load access mode')
|
||||
;(useGetInstalledAppAccessModeByAppId as Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
data: null,
|
||||
error,
|
||||
})
|
||||
@ -444,7 +456,7 @@ describe('InstalledApp', () => {
|
||||
|
||||
it('should not update app params when data is null', async () => {
|
||||
;(useGetInstalledAppParams as Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
data: null,
|
||||
error: null,
|
||||
})
|
||||
@ -460,7 +472,7 @@ describe('InstalledApp', () => {
|
||||
|
||||
it('should not update app meta when data is null', async () => {
|
||||
;(useGetInstalledAppMeta as Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
data: null,
|
||||
error: null,
|
||||
})
|
||||
@ -476,7 +488,7 @@ describe('InstalledApp', () => {
|
||||
|
||||
it('should not update access mode when data is null', async () => {
|
||||
;(useGetInstalledAppAccessModeByAppId as Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
data: null,
|
||||
error: null,
|
||||
})
|
||||
@ -557,7 +569,7 @@ describe('InstalledApp', () => {
|
||||
describe('Render Priority', () => {
|
||||
it('should show error before loading state', () => {
|
||||
;(useGetInstalledAppParams as Mock).mockReturnValue({
|
||||
isLoading: true,
|
||||
isPending: true,
|
||||
data: null,
|
||||
error: new Error('Some error'),
|
||||
})
|
||||
@ -568,7 +580,7 @@ describe('InstalledApp', () => {
|
||||
|
||||
it('should show error before permission check', () => {
|
||||
;(useGetInstalledAppParams as Mock).mockReturnValue({
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
data: null,
|
||||
error: new Error('Params error'),
|
||||
})
|
||||
@ -594,13 +606,8 @@ describe('InstalledApp', () => {
|
||||
expect(screen.queryByText(/404/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show loading before 404', () => {
|
||||
setupMocks([])
|
||||
;(useGetInstalledAppParams as Mock).mockReturnValue({
|
||||
isLoading: true,
|
||||
data: null,
|
||||
error: null,
|
||||
})
|
||||
it('should show loading before 404 while installed apps are refetching', () => {
|
||||
setupMocks([], { isFetching: true })
|
||||
|
||||
const { container } = render(<InstalledApp id="nonexistent-app" />)
|
||||
const svg = container.querySelector('svg.spin-animation')
|
||||
|
||||
@ -17,7 +17,7 @@ const InstalledApp = ({
|
||||
}: {
|
||||
id: string
|
||||
}) => {
|
||||
const { data, isPending: isPendingInstalledApps } = useGetInstalledApps()
|
||||
const { data, isPending: isPendingInstalledApps, isFetching: isFetchingInstalledApps } = useGetInstalledApps()
|
||||
const installedApp = data?.installed_apps?.find(item => item.id === id)
|
||||
const updateAppInfo = useWebAppStore(s => s.updateAppInfo)
|
||||
const updateWebAppAccessMode = useWebAppStore(s => s.updateWebAppAccessMode)
|
||||
@ -97,7 +97,11 @@ const InstalledApp = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (isPendingInstalledApps || (installedApp && (isPendingAppParams || isPendingAppMeta || isPendingWebAppAccessMode))) {
|
||||
if (
|
||||
isPendingInstalledApps
|
||||
|| (!installedApp && isFetchingInstalledApps)
|
||||
|| (installedApp && (isPendingAppParams || isPendingAppMeta || isPendingWebAppAccessMode))
|
||||
) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<Loading />
|
||||
|
||||
Reference in New Issue
Block a user