fix(web): preserve form state during config refetch (#37357)

This commit is contained in:
KVOJJJin 2026-06-12 11:21:33 +08:00 committed by GitHub
parent a650ffc00a
commit 72faca2592
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 166 additions and 4 deletions

View File

@ -0,0 +1,162 @@
import type { AppData, AppMeta } from '@/models/share'
import { render, screen } from '@testing-library/react'
import AuthenticatedLayout from '../authenticated-layout'
type QueryState<TData> = {
data?: TData
error?: Error | null
isLoading: boolean
isFetching: boolean
}
type UserCanAccessApp = {
result: boolean
}
const updateAppInfo = vi.fn()
const updateAppParams = vi.fn()
const updateWebAppMeta = vi.fn()
const updateUserCanAccessApp = vi.fn()
const mockWebAppState = {
shareCode: 'share-code',
updateAppInfo,
updateAppParams,
updateWebAppMeta,
updateUserCanAccessApp,
}
const appInfoQueryState: QueryState<AppData> = {
data: {
app_id: 'app-id',
custom_config: null,
site: {
title: 'Workflow App',
},
},
error: null,
isLoading: false,
isFetching: false,
}
const appParamsQueryState: QueryState<Record<string, unknown>> = {
data: {
user_input_form: [],
},
error: null,
isLoading: false,
isFetching: false,
}
const appMetaQueryState: QueryState<AppMeta> = {
data: {
tool_icons: {},
},
error: null,
isLoading: false,
isFetching: false,
}
const userCanAccessAppQueryState: QueryState<UserCanAccessApp> = {
data: {
result: true,
},
error: null,
isLoading: false,
isFetching: false,
}
vi.mock('@/context/web-app-context', () => ({
useWebAppStore: (selector: (state: typeof mockWebAppState) => unknown) => selector(mockWebAppState),
}))
vi.mock('@/next/navigation', () => ({
usePathname: () => '/workflow/share-code',
useRouter: () => ({
replace: vi.fn(),
}),
useSearchParams: () => new URLSearchParams(),
}))
vi.mock('@/service/use-share', () => ({
useGetWebAppInfo: () => appInfoQueryState,
useGetWebAppParams: () => appParamsQueryState,
useGetWebAppMeta: () => appMetaQueryState,
}))
vi.mock('@/service/access-control', () => ({
useGetUserCanAccessApp: () => userCanAccessAppQueryState,
}))
vi.mock('@/service/webapp-auth', () => ({
webAppLogout: vi.fn(),
}))
const resetQueryStates = () => {
appInfoQueryState.data = {
app_id: 'app-id',
custom_config: null,
site: {
title: 'Workflow App',
},
}
appInfoQueryState.error = null
appInfoQueryState.isLoading = false
appInfoQueryState.isFetching = false
appParamsQueryState.data = {
user_input_form: [],
}
appParamsQueryState.error = null
appParamsQueryState.isLoading = false
appParamsQueryState.isFetching = false
appMetaQueryState.data = {
tool_icons: {},
}
appMetaQueryState.error = null
appMetaQueryState.isLoading = false
appMetaQueryState.isFetching = false
userCanAccessAppQueryState.data = {
result: true,
}
userCanAccessAppQueryState.error = null
userCanAccessAppQueryState.isLoading = false
userCanAccessAppQueryState.isFetching = false
}
const renderLayout = () => render(
<AuthenticatedLayout>
<div>Workflow form content</div>
</AuthenticatedLayout>,
)
describe('AuthenticatedLayout', () => {
beforeEach(() => {
vi.clearAllMocks()
resetQueryStates()
})
describe('Loading State', () => {
it('should keep children mounted when existing app config is background refetching', () => {
appInfoQueryState.isFetching = true
appParamsQueryState.isFetching = true
appMetaQueryState.isFetching = true
renderLayout()
expect(screen.getByText('Workflow form content')).toBeInTheDocument()
})
it('should hide children while initial app config is loading', () => {
appInfoQueryState.data = undefined
appInfoQueryState.isLoading = true
appInfoQueryState.isFetching = true
renderLayout()
expect(screen.queryByText('Workflow form content')).not.toBeInTheDocument()
})
})
})

View File

@ -18,9 +18,9 @@ const AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => {
const updateAppParams = useWebAppStore(s => s.updateAppParams)
const updateWebAppMeta = useWebAppStore(s => s.updateWebAppMeta)
const updateUserCanAccessApp = useWebAppStore(s => s.updateUserCanAccessApp)
const { isFetching: isFetchingAppParams, data: appParams, error: appParamsError } = useGetWebAppParams()
const { isFetching: isFetchingAppInfo, data: appInfo, error: appInfoError } = useGetWebAppInfo()
const { isFetching: isFetchingAppMeta, data: appMeta, error: appMetaError } = useGetWebAppMeta()
const { isLoading: isLoadingAppParams, data: appParams, error: appParamsError } = useGetWebAppParams()
const { isLoading: isLoadingAppInfo, data: appInfo, error: appInfoError } = useGetWebAppInfo()
const { isLoading: isLoadingAppMeta, data: appMeta, error: appMetaError } = useGetWebAppMeta()
const { data: userCanAccessApp, error: useCanAccessAppError } = useGetUserCanAccessApp({ appId: appInfo?.app_id, isInstalledApp: false })
useEffect(() => {
@ -87,7 +87,7 @@ const AuthenticatedLayout = ({ children }: { children: React.ReactNode }) => {
</div>
)
}
if (isFetchingAppInfo || isFetchingAppParams || isFetchingAppMeta) {
if (isLoadingAppInfo || isLoadingAppParams || isLoadingAppMeta || !appInfo || !appParams || !appMeta) {
return (
<div className="flex h-full items-center justify-center">
<Loading />