mirror of
https://github.com/langgenius/dify.git
synced 2026-05-10 14:14:17 +08:00
tweaks
This commit is contained in:
parent
60bae05d0f
commit
b80e944665
75
web/app/(shareLayout)/webapp-signin/__tests__/page.spec.tsx
Normal file
75
web/app/(shareLayout)/webapp-signin/__tests__/page.spec.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import WebSSOForm from '../page'
|
||||
|
||||
const mockReplace = vi.fn()
|
||||
let mockRedirectUrl = '/share/test-share-code'
|
||||
let mockWebAppAccessMode: AccessMode | null = null
|
||||
let mockSystemFeaturesEnabled = true
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
replace: mockReplace,
|
||||
}),
|
||||
useSearchParams: () => ({
|
||||
get: (key: string) => key === 'redirect_url' ? mockRedirectUrl : null,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/global-public-context', () => ({
|
||||
useGlobalPublicStore: (selector: (state: { systemFeatures: { webapp_auth: { enabled: boolean } } }) => unknown) =>
|
||||
selector({
|
||||
systemFeatures: {
|
||||
webapp_auth: {
|
||||
enabled: mockSystemFeaturesEnabled,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/web-app-context', () => ({
|
||||
useWebAppStore: (selector: (state: { webAppAccessMode: AccessMode | null, shareCode: string | null }) => unknown) =>
|
||||
selector({
|
||||
webAppAccessMode: mockWebAppAccessMode,
|
||||
shareCode: 'test-share-code',
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/webapp-auth', () => ({
|
||||
webAppLogout: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../normalForm', () => ({
|
||||
default: () => <div data-testid="normal-form" />,
|
||||
}))
|
||||
|
||||
vi.mock('../components/external-member-sso-auth', () => ({
|
||||
default: () => <div data-testid="external-member-sso-auth" />,
|
||||
}))
|
||||
|
||||
describe('WebSSOForm', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockRedirectUrl = '/share/test-share-code'
|
||||
mockWebAppAccessMode = null
|
||||
mockSystemFeaturesEnabled = true
|
||||
})
|
||||
|
||||
describe('Access Mode Resolution', () => {
|
||||
it('should avoid rendering auth variants before the access mode query resolves', () => {
|
||||
render(<WebSSOForm />)
|
||||
|
||||
expect(screen.queryByTestId('normal-form')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('external-member-sso-auth')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('share.login.backToHome')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the normal form for organization-backed access modes', () => {
|
||||
mockWebAppAccessMode = AccessMode.ORGANIZATION
|
||||
|
||||
render(<WebSSOForm />)
|
||||
|
||||
expect(screen.getByTestId('normal-form')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -45,10 +45,13 @@ const WebSSOForm: FC = () => {
|
||||
if (!systemFeatures.webapp_auth.enabled) {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<p className="system-xs-regular text-text-tertiary">{t('webapp.disabled', { ns: 'login' })}</p>
|
||||
<p className="text-text-tertiary system-xs-regular">{t('webapp.disabled', { ns: 'login' })}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (webAppAccessMode === null)
|
||||
return <div className="w-full max-w-[400px]" />
|
||||
|
||||
if (webAppAccessMode && (webAppAccessMode === AccessMode.ORGANIZATION || webAppAccessMode === AccessMode.SPECIFIC_GROUPS_MEMBERS)) {
|
||||
return (
|
||||
<div className="w-full max-w-[400px]">
|
||||
@ -63,7 +66,7 @@ const WebSSOForm: FC = () => {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-y-4">
|
||||
<AppUnavailable className="h-auto w-auto" isUnknownReason={true} />
|
||||
<span className="system-sm-regular cursor-pointer text-text-tertiary" onClick={backToHome}>{t('login.backToHome', { ns: 'share' })}</span>
|
||||
<span className="cursor-pointer text-text-tertiary system-sm-regular" onClick={backToHome}>{t('login.backToHome', { ns: 'share' })}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import type { SiteInfo } from '@/models/share'
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import MenuDropdown from '../menu-dropdown'
|
||||
|
||||
const mockReplace = vi.fn()
|
||||
const mockPathname = '/test-path'
|
||||
let mockWebAppAccessMode: AccessMode | null = AccessMode.SPECIFIC_GROUPS_MEMBERS
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
replace: mockReplace,
|
||||
@ -16,7 +18,7 @@ const mockShareCode = 'test-share-code'
|
||||
vi.mock('@/context/web-app-context', () => ({
|
||||
useWebAppStore: (selector: (state: Record<string, unknown>) => unknown) => {
|
||||
const state = {
|
||||
webAppAccessMode: 'code',
|
||||
webAppAccessMode: mockWebAppAccessMode,
|
||||
shareCode: mockShareCode,
|
||||
}
|
||||
return selector(state)
|
||||
@ -41,6 +43,7 @@ describe('MenuDropdown', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockWebAppAccessMode = AccessMode.SPECIFIC_GROUPS_MEMBERS
|
||||
})
|
||||
|
||||
describe('rendering', () => {
|
||||
@ -151,6 +154,19 @@ describe('MenuDropdown', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should hide logout option when access mode is unknown', async () => {
|
||||
mockWebAppAccessMode = null
|
||||
|
||||
render(<MenuDropdown data={baseSiteInfo} hideLogout={false} />)
|
||||
|
||||
const triggerButton = screen.getByRole('button')
|
||||
fireEvent.click(triggerButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('common.userProfile.logout')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should call webAppLogout and redirect when logout is clicked', async () => {
|
||||
render(<MenuDropdown data={baseSiteInfo} hideLogout={false} />)
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { AppSourceType } from '@/service/share'
|
||||
import { useTextGenerationAppState } from '../use-text-generation-app-state'
|
||||
|
||||
@ -118,13 +119,13 @@ const defaultAppParams = {
|
||||
type MockWebAppState = {
|
||||
appInfo: MockAppInfo | null
|
||||
appParams: typeof defaultAppParams | null
|
||||
webAppAccessMode: string
|
||||
webAppAccessMode: AccessMode | null
|
||||
}
|
||||
|
||||
const mockWebAppState: MockWebAppState = {
|
||||
appInfo: defaultAppInfo,
|
||||
appParams: defaultAppParams,
|
||||
webAppAccessMode: 'public',
|
||||
webAppAccessMode: AccessMode.PUBLIC,
|
||||
}
|
||||
|
||||
const resetMockWebAppState = () => {
|
||||
@ -154,7 +155,7 @@ const resetMockWebAppState = () => {
|
||||
image_file_size_limit: 10,
|
||||
},
|
||||
}
|
||||
mockWebAppState.webAppAccessMode = 'public'
|
||||
mockWebAppState.webAppAccessMode = AccessMode.PUBLIC
|
||||
}
|
||||
|
||||
vi.mock('@/context/global-public-context', () => ({
|
||||
|
||||
@ -58,6 +58,7 @@ const MenuDropdown: FC<Props> = ({
|
||||
}, [router, pathname, webAppLogout, shareCode])
|
||||
|
||||
const [show, setShow] = useState(false)
|
||||
const showLogout = !hideLogout && webAppAccessMode !== null && webAppAccessMode !== AccessMode.EXTERNAL_MEMBERS && webAppAccessMode !== AccessMode.PUBLIC
|
||||
|
||||
useEffect(() => {
|
||||
if (forceClose)
|
||||
@ -85,7 +86,7 @@ const MenuDropdown: FC<Props> = ({
|
||||
<PortalToFollowElemContent className="z-50">
|
||||
<div className="w-[224px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm">
|
||||
<div className="p-1">
|
||||
<div className={cn('system-md-regular flex cursor-pointer items-center rounded-lg py-1.5 pl-3 pr-2 text-text-secondary')}>
|
||||
<div className={cn('flex cursor-pointer items-center rounded-lg py-1.5 pl-3 pr-2 text-text-secondary system-md-regular')}>
|
||||
<div className="grow">{t('theme.theme', { ns: 'common' })}</div>
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
@ -93,7 +94,7 @@ const MenuDropdown: FC<Props> = ({
|
||||
<Divider type="horizontal" className="my-0" />
|
||||
<div className="p-1">
|
||||
{data?.privacy_policy && (
|
||||
<a href={data.privacy_policy} target="_blank" className="system-md-regular flex cursor-pointer items-center rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover">
|
||||
<a href={data.privacy_policy} target="_blank" className="flex cursor-pointer items-center rounded-lg px-3 py-1.5 text-text-secondary system-md-regular hover:bg-state-base-hover">
|
||||
<span className="grow">{t('chat.privacyPolicyMiddle', { ns: 'share' })}</span>
|
||||
</a>
|
||||
)}
|
||||
@ -102,16 +103,16 @@ const MenuDropdown: FC<Props> = ({
|
||||
handleTrigger()
|
||||
setShow(true)
|
||||
}}
|
||||
className="system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover"
|
||||
className="cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary system-md-regular hover:bg-state-base-hover"
|
||||
>
|
||||
{t('userProfile.about', { ns: 'common' })}
|
||||
</div>
|
||||
</div>
|
||||
{!(hideLogout || webAppAccessMode === AccessMode.EXTERNAL_MEMBERS || webAppAccessMode === AccessMode.PUBLIC) && (
|
||||
{showLogout && (
|
||||
<div className="p-1">
|
||||
<div
|
||||
onClick={handleLogout}
|
||||
className="system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover"
|
||||
className="cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary system-md-regular hover:bg-state-base-hover"
|
||||
>
|
||||
{t('userProfile.logout', { ns: 'common' })}
|
||||
</div>
|
||||
|
||||
@ -18,7 +18,7 @@ import RunBatch from './run-batch'
|
||||
import RunOnce from './run-once'
|
||||
|
||||
type TextGenerationSidebarProps = {
|
||||
accessMode: AccessMode
|
||||
accessMode: AccessMode | null
|
||||
allTasksRun: boolean
|
||||
currentTab: string
|
||||
customConfig: TextGenerationCustomConfig | null
|
||||
|
||||
108
web/context/__tests__/web-app-context.spec.tsx
Normal file
108
web/context/__tests__/web-app-context.spec.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import WebAppStoreProvider, { useWebAppStore } from '@/context/web-app-context'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
|
||||
let mockPathname = '/share/test-share-code'
|
||||
let mockRedirectUrl: string | null = null
|
||||
let mockAccessModeResult: { accessMode: AccessMode } | undefined
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
usePathname: () => mockPathname,
|
||||
useSearchParams: () => ({
|
||||
get: (key: string) => key === 'redirect_url' ? mockRedirectUrl : null,
|
||||
toString: () => {
|
||||
const params = new URLSearchParams()
|
||||
if (mockRedirectUrl)
|
||||
params.set('redirect_url', mockRedirectUrl)
|
||||
return params.toString()
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/chat/utils', () => ({
|
||||
getProcessedSystemVariablesFromUrlParams: vi.fn().mockResolvedValue({}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-share', () => ({
|
||||
useGetWebAppAccessModeByCode: vi.fn(() => ({
|
||||
data: mockAccessModeResult,
|
||||
})),
|
||||
}))
|
||||
|
||||
const StoreSnapshot = () => {
|
||||
const shareCode = useWebAppStore(s => s.shareCode)
|
||||
const accessMode = useWebAppStore(s => s.webAppAccessMode)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span data-testid="share-code">{shareCode ?? 'none'}</span>
|
||||
<span data-testid="access-mode">{accessMode ?? 'unknown'}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
describe('WebAppStoreProvider', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockPathname = '/share/test-share-code'
|
||||
mockRedirectUrl = null
|
||||
mockAccessModeResult = undefined
|
||||
useWebAppStore.setState({
|
||||
shareCode: null,
|
||||
appInfo: null,
|
||||
appParams: null,
|
||||
webAppAccessMode: null,
|
||||
appMeta: null,
|
||||
userCanAccessApp: false,
|
||||
embeddedUserId: null,
|
||||
embeddedConversationId: null,
|
||||
})
|
||||
})
|
||||
|
||||
describe('Access Mode State', () => {
|
||||
it('should keep the access mode unknown until the query resolves', async () => {
|
||||
render(
|
||||
<WebAppStoreProvider>
|
||||
<StoreSnapshot />
|
||||
</WebAppStoreProvider>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('share-code')).toHaveTextContent('test-share-code')
|
||||
})
|
||||
expect(screen.getByTestId('access-mode')).toHaveTextContent('unknown')
|
||||
})
|
||||
|
||||
it('should reset the access mode when the share code changes before the next result arrives', async () => {
|
||||
const { rerender } = render(
|
||||
<WebAppStoreProvider>
|
||||
<StoreSnapshot />
|
||||
</WebAppStoreProvider>,
|
||||
)
|
||||
|
||||
mockAccessModeResult = { accessMode: AccessMode.PUBLIC }
|
||||
rerender(
|
||||
<WebAppStoreProvider>
|
||||
<StoreSnapshot />
|
||||
</WebAppStoreProvider>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('access-mode')).toHaveTextContent(AccessMode.PUBLIC)
|
||||
})
|
||||
|
||||
mockPathname = '/share/next-share-code'
|
||||
mockAccessModeResult = undefined
|
||||
rerender(
|
||||
<WebAppStoreProvider>
|
||||
<StoreSnapshot />
|
||||
</WebAppStoreProvider>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('share-code')).toHaveTextContent('next-share-code')
|
||||
})
|
||||
expect(screen.getByTestId('access-mode')).toHaveTextContent('unknown')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -2,11 +2,11 @@
|
||||
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
import type { ChatConfig } from '@/app/components/base/chat/types'
|
||||
import type { AccessMode } from '@/models/access-control'
|
||||
import type { AppData, AppMeta } from '@/models/share'
|
||||
import { useEffect } from 'react'
|
||||
import { create } from 'zustand'
|
||||
import { getProcessedSystemVariablesFromUrlParams } from '@/app/components/base/chat/utils'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { usePathname, useSearchParams } from '@/next/navigation'
|
||||
import { useGetWebAppAccessModeByCode } from '@/service/use-share'
|
||||
|
||||
@ -17,8 +17,8 @@ type WebAppStore = {
|
||||
updateAppInfo: (appInfo: AppData | null) => void
|
||||
appParams: ChatConfig | null
|
||||
updateAppParams: (appParams: ChatConfig | null) => void
|
||||
webAppAccessMode: AccessMode
|
||||
updateWebAppAccessMode: (accessMode: AccessMode) => void
|
||||
webAppAccessMode: AccessMode | null
|
||||
updateWebAppAccessMode: (accessMode: AccessMode | null) => void
|
||||
appMeta: AppMeta | null
|
||||
updateWebAppMeta: (appMeta: AppMeta | null) => void
|
||||
userCanAccessApp: boolean
|
||||
@ -36,8 +36,8 @@ export const useWebAppStore = create<WebAppStore>(set => ({
|
||||
updateAppInfo: (appInfo: AppData | null) => set(() => ({ appInfo })),
|
||||
appParams: null,
|
||||
updateAppParams: (appParams: ChatConfig | null) => set(() => ({ appParams })),
|
||||
webAppAccessMode: AccessMode.SPECIFIC_GROUPS_MEMBERS,
|
||||
updateWebAppAccessMode: (accessMode: AccessMode) => set(() => ({ webAppAccessMode: accessMode })),
|
||||
webAppAccessMode: null,
|
||||
updateWebAppAccessMode: (accessMode: AccessMode | null) => set(() => ({ webAppAccessMode: accessMode })),
|
||||
appMeta: null,
|
||||
updateWebAppMeta: (appMeta: AppMeta | null) => set(() => ({ appMeta })),
|
||||
userCanAccessApp: false,
|
||||
@ -78,6 +78,10 @@ const WebAppStoreProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
updateShareCode(shareCode)
|
||||
}, [shareCode, updateShareCode])
|
||||
|
||||
useEffect(() => {
|
||||
updateWebAppAccessMode(null)
|
||||
}, [shareCode, updateWebAppAccessMode])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
const syncEmbeddedUserId = async () => {
|
||||
@ -106,7 +110,7 @@ const WebAppStoreProvider: FC<PropsWithChildren> = ({ children }) => {
|
||||
useEffect(() => {
|
||||
if (accessModeResult?.accessMode)
|
||||
updateWebAppAccessMode(accessModeResult.accessMode)
|
||||
}, [accessModeResult, updateWebAppAccessMode, shareCode])
|
||||
}, [accessModeResult, updateWebAppAccessMode])
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user