mirror of
https://github.com/langgenius/dify.git
synced 2026-05-01 07:58:02 +08:00
test: add unit tests for access control components to enhance coverage and reliability
- Introduced new test files for AccessControlDialog, AccessControlItem, AddMemberOrGroupDialog, and SpecificGroupsOrMembers. - Enhanced testing for rendering, interactions, and state management across various access control components. - Ensured comprehensive coverage for user interactions and state updates in the access control context.
This commit is contained in:
@ -0,0 +1,16 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import HitHistoryNoData from '../hit-history-no-data'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('HitHistoryNoData', () => {
|
||||
it('should render the empty history message', () => {
|
||||
render(<HitHistoryNoData />)
|
||||
|
||||
expect(screen.getByText('appAnnotation.viewModal.noHitHistory')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,52 @@
|
||||
/* eslint-disable ts/no-explicit-any */
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import AccessControlDialog from '../access-control-dialog'
|
||||
|
||||
vi.mock('@headlessui/react', () => {
|
||||
const DialogComponent: any = ({ children, className, ...rest }: any) => (
|
||||
<div role="dialog" className={className} {...rest}>{children}</div>
|
||||
)
|
||||
DialogComponent.Panel = ({ children, className, ...rest }: any) => (
|
||||
<div className={className} {...rest}>{children}</div>
|
||||
)
|
||||
const TransitionChild = ({ children }: any) => (
|
||||
<>{typeof children === 'function' ? children({}) : children}</>
|
||||
)
|
||||
const Transition = ({ show = true, children }: any) => (
|
||||
show ? <>{typeof children === 'function' ? children({}) : children}</> : null
|
||||
)
|
||||
Transition.Child = TransitionChild
|
||||
return {
|
||||
Dialog: DialogComponent,
|
||||
Transition,
|
||||
}
|
||||
})
|
||||
|
||||
describe('AccessControlDialog', () => {
|
||||
it('should render dialog content when visible', () => {
|
||||
render(
|
||||
<AccessControlDialog show className="custom-dialog">
|
||||
<div>Dialog Content</div>
|
||||
</AccessControlDialog>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
||||
expect(screen.getByText('Dialog Content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should trigger onClose when clicking the close control', async () => {
|
||||
const onClose = vi.fn()
|
||||
const { container } = render(
|
||||
<AccessControlDialog show onClose={onClose}>
|
||||
<div>Dialog Content</div>
|
||||
</AccessControlDialog>,
|
||||
)
|
||||
|
||||
const closeButton = container.querySelector('.absolute.right-5.top-5') as HTMLElement
|
||||
fireEvent.click(closeButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,45 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import useAccessControlStore from '@/context/access-control-store'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import AccessControlItem from '../access-control-item'
|
||||
|
||||
describe('AccessControlItem', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
useAccessControlStore.setState({
|
||||
appId: '',
|
||||
specificGroups: [],
|
||||
specificMembers: [],
|
||||
currentMenu: AccessMode.PUBLIC,
|
||||
selectedGroupsForBreadcrumb: [],
|
||||
})
|
||||
})
|
||||
|
||||
it('should update current menu when selecting a different access type', () => {
|
||||
render(
|
||||
<AccessControlItem type={AccessMode.ORGANIZATION}>
|
||||
<span>Organization Only</span>
|
||||
</AccessControlItem>,
|
||||
)
|
||||
|
||||
const option = screen.getByText('Organization Only').parentElement as HTMLElement
|
||||
fireEvent.click(option)
|
||||
|
||||
expect(useAccessControlStore.getState().currentMenu).toBe(AccessMode.ORGANIZATION)
|
||||
})
|
||||
|
||||
it('should keep the selected state for the active access type', () => {
|
||||
useAccessControlStore.setState({
|
||||
currentMenu: AccessMode.ORGANIZATION,
|
||||
})
|
||||
|
||||
render(
|
||||
<AccessControlItem type={AccessMode.ORGANIZATION}>
|
||||
<span>Organization Only</span>
|
||||
</AccessControlItem>,
|
||||
)
|
||||
|
||||
const option = screen.getByText('Organization Only').parentElement as HTMLElement
|
||||
expect(option).toHaveClass('border-components-option-card-option-selected-border')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,138 @@
|
||||
import type { AccessControlAccount, AccessControlGroup, Subject } from '@/models/access-control'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import useAccessControlStore from '@/context/access-control-store'
|
||||
import { SubjectType } from '@/models/access-control'
|
||||
import AddMemberOrGroupDialog from '../add-member-or-group-pop'
|
||||
|
||||
const mockUseSearchForWhiteListCandidates = vi.fn()
|
||||
const intersectionObserverMocks = vi.hoisted(() => ({
|
||||
callback: null as null | ((entries: Array<{ isIntersecting: boolean }>) => void),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useSelector: <T,>(selector: (value: { userProfile: { email: string } }) => T) => selector({
|
||||
userProfile: {
|
||||
email: 'member@example.com',
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('ahooks', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('ahooks')>()
|
||||
return {
|
||||
...actual,
|
||||
useDebounce: (value: unknown) => value,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/service/access-control', () => ({
|
||||
useSearchForWhiteListCandidates: (...args: unknown[]) => mockUseSearchForWhiteListCandidates(...args),
|
||||
}))
|
||||
|
||||
const createGroup = (overrides: Partial<AccessControlGroup> = {}): AccessControlGroup => ({
|
||||
id: 'group-1',
|
||||
name: 'Group One',
|
||||
groupSize: 5,
|
||||
...overrides,
|
||||
} as AccessControlGroup)
|
||||
|
||||
const createMember = (overrides: Partial<AccessControlAccount> = {}): AccessControlAccount => ({
|
||||
id: 'member-1',
|
||||
name: 'Member One',
|
||||
email: 'member@example.com',
|
||||
avatar: '',
|
||||
avatarUrl: '',
|
||||
...overrides,
|
||||
} as AccessControlAccount)
|
||||
|
||||
describe('AddMemberOrGroupDialog', () => {
|
||||
const baseGroup = createGroup()
|
||||
const baseMember = createMember()
|
||||
const groupSubject: Subject = {
|
||||
subjectId: baseGroup.id,
|
||||
subjectType: SubjectType.GROUP,
|
||||
groupData: baseGroup,
|
||||
} as Subject
|
||||
const memberSubject: Subject = {
|
||||
subjectId: baseMember.id,
|
||||
subjectType: SubjectType.ACCOUNT,
|
||||
accountData: baseMember,
|
||||
} as Subject
|
||||
|
||||
beforeAll(() => {
|
||||
class MockIntersectionObserver {
|
||||
constructor(callback: (entries: Array<{ isIntersecting: boolean }>) => void) {
|
||||
intersectionObserverMocks.callback = callback
|
||||
}
|
||||
|
||||
observe = vi.fn(() => undefined)
|
||||
disconnect = vi.fn(() => undefined)
|
||||
unobserve = vi.fn(() => undefined)
|
||||
}
|
||||
|
||||
// @ts-expect-error test DOM typings do not guarantee IntersectionObserver here
|
||||
globalThis.IntersectionObserver = MockIntersectionObserver
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
useAccessControlStore.setState({
|
||||
appId: 'app-1',
|
||||
specificGroups: [],
|
||||
specificMembers: [],
|
||||
currentMenu: SubjectType.GROUP as never,
|
||||
selectedGroupsForBreadcrumb: [],
|
||||
})
|
||||
mockUseSearchForWhiteListCandidates.mockReturnValue({
|
||||
isLoading: false,
|
||||
isFetchingNextPage: false,
|
||||
fetchNextPage: vi.fn(),
|
||||
data: {
|
||||
pages: [{ currPage: 1, subjects: [groupSubject, memberSubject], hasMore: false }],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should open the search popover and display candidates', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<AddMemberOrGroupDialog />)
|
||||
|
||||
await user.click(screen.getByText('common.operation.add'))
|
||||
|
||||
expect(screen.getByPlaceholderText('app.accessControlDialog.operateGroupAndMember.searchPlaceholder')).toBeInTheDocument()
|
||||
expect(screen.getByText(baseGroup.name)).toBeInTheDocument()
|
||||
expect(screen.getByText(baseMember.name)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should allow expanding groups and selecting members', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<AddMemberOrGroupDialog />)
|
||||
|
||||
await user.click(screen.getByText('common.operation.add'))
|
||||
await user.click(screen.getByText('app.accessControlDialog.operateGroupAndMember.expand'))
|
||||
|
||||
expect(useAccessControlStore.getState().selectedGroupsForBreadcrumb).toEqual([baseGroup])
|
||||
|
||||
const memberCheckbox = screen.getByText(baseMember.name).parentElement?.previousElementSibling as HTMLElement
|
||||
fireEvent.click(memberCheckbox)
|
||||
|
||||
expect(useAccessControlStore.getState().specificMembers).toEqual([baseMember])
|
||||
})
|
||||
|
||||
it('should show the empty state when no candidates are returned', async () => {
|
||||
mockUseSearchForWhiteListCandidates.mockReturnValue({
|
||||
isLoading: false,
|
||||
isFetchingNextPage: false,
|
||||
fetchNextPage: vi.fn(),
|
||||
data: { pages: [] },
|
||||
})
|
||||
|
||||
const user = userEvent.setup()
|
||||
render(<AddMemberOrGroupDialog />)
|
||||
|
||||
await user.click(screen.getByText('common.operation.add'))
|
||||
|
||||
expect(screen.getByText('app.accessControlDialog.operateGroupAndMember.noResult')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,149 @@
|
||||
/* eslint-disable ts/no-explicit-any */
|
||||
import type { App } from '@/types/app'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { toast } from '@/app/components/base/ui/toast'
|
||||
import useAccessControlStore from '@/context/access-control-store'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import AccessControl from '../index'
|
||||
|
||||
const mockMutateAsync = vi.fn()
|
||||
const mockUseUpdateAccessMode = vi.fn(() => ({
|
||||
isPending: false,
|
||||
mutateAsync: mockMutateAsync,
|
||||
}))
|
||||
const mockUseAppWhiteListSubjects = vi.fn()
|
||||
const mockUseSearchForWhiteListCandidates = vi.fn()
|
||||
let mockWebappAuth = {
|
||||
enabled: true,
|
||||
allow_sso: true,
|
||||
allow_email_password_login: false,
|
||||
allow_email_code_login: false,
|
||||
}
|
||||
|
||||
vi.mock('@headlessui/react', () => {
|
||||
const DialogComponent: any = ({ children, className, ...rest }: any) => (
|
||||
<div role="dialog" className={className} {...rest}>{children}</div>
|
||||
)
|
||||
DialogComponent.Panel = ({ children, className, ...rest }: any) => (
|
||||
<div className={className} {...rest}>{children}</div>
|
||||
)
|
||||
const DialogTitle = ({ children, className, ...rest }: any) => (
|
||||
<div className={className} {...rest}>{children}</div>
|
||||
)
|
||||
const DialogDescription = ({ children, className, ...rest }: any) => (
|
||||
<div className={className} {...rest}>{children}</div>
|
||||
)
|
||||
const TransitionChild = ({ children }: any) => (
|
||||
<>{typeof children === 'function' ? children({}) : children}</>
|
||||
)
|
||||
const Transition = ({ show = true, children }: any) => (
|
||||
show ? <>{typeof children === 'function' ? children({}) : children}</> : null
|
||||
)
|
||||
Transition.Child = TransitionChild
|
||||
return {
|
||||
Dialog: DialogComponent,
|
||||
Transition,
|
||||
DialogTitle,
|
||||
Description: DialogDescription,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/context/global-public-context', () => ({
|
||||
useGlobalPublicStore: (selector: (state: { systemFeatures: { webapp_auth: typeof mockWebappAuth } }) => unknown) => selector({
|
||||
systemFeatures: {
|
||||
webapp_auth: mockWebappAuth,
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/access-control', () => ({
|
||||
useAppWhiteListSubjects: (...args: unknown[]) => mockUseAppWhiteListSubjects(...args),
|
||||
useSearchForWhiteListCandidates: (...args: unknown[]) => mockUseSearchForWhiteListCandidates(...args),
|
||||
useUpdateAccessMode: () => mockUseUpdateAccessMode(),
|
||||
}))
|
||||
|
||||
describe('AccessControl', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockWebappAuth = {
|
||||
enabled: true,
|
||||
allow_sso: true,
|
||||
allow_email_password_login: false,
|
||||
allow_email_code_login: false,
|
||||
}
|
||||
useAccessControlStore.setState({
|
||||
appId: '',
|
||||
specificGroups: [],
|
||||
specificMembers: [],
|
||||
currentMenu: AccessMode.SPECIFIC_GROUPS_MEMBERS,
|
||||
selectedGroupsForBreadcrumb: [],
|
||||
})
|
||||
mockMutateAsync.mockResolvedValue(undefined)
|
||||
mockUseAppWhiteListSubjects.mockReturnValue({
|
||||
isPending: false,
|
||||
data: {
|
||||
groups: [],
|
||||
members: [],
|
||||
},
|
||||
})
|
||||
mockUseSearchForWhiteListCandidates.mockReturnValue({
|
||||
isLoading: false,
|
||||
isFetchingNextPage: false,
|
||||
fetchNextPage: vi.fn(),
|
||||
data: { pages: [] },
|
||||
})
|
||||
})
|
||||
|
||||
it('should initialize menu from the app and update access mode on confirm', async () => {
|
||||
const onClose = vi.fn()
|
||||
const onConfirm = vi.fn()
|
||||
const toastSpy = vi.spyOn(toast, 'success').mockReturnValue('toast-success')
|
||||
const app = {
|
||||
id: 'app-id-1',
|
||||
access_mode: AccessMode.PUBLIC,
|
||||
} as App
|
||||
|
||||
render(
|
||||
<AccessControl
|
||||
app={app}
|
||||
onClose={onClose}
|
||||
onConfirm={onConfirm}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(useAccessControlStore.getState().appId).toBe(app.id)
|
||||
expect(useAccessControlStore.getState().currentMenu).toBe(AccessMode.PUBLIC)
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText('common.operation.confirm'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith({
|
||||
appId: app.id,
|
||||
accessMode: AccessMode.PUBLIC,
|
||||
})
|
||||
expect(toastSpy).toHaveBeenCalledWith('app.accessControlDialog.updateSuccess')
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('should show the external-members option when SSO tip is visible', () => {
|
||||
mockWebappAuth = {
|
||||
enabled: false,
|
||||
allow_sso: false,
|
||||
allow_email_password_login: false,
|
||||
allow_email_code_login: false,
|
||||
}
|
||||
|
||||
render(
|
||||
<AccessControl
|
||||
app={{ id: 'app-id-2', access_mode: AccessMode.PUBLIC } as App}
|
||||
onClose={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('app.accessControlDialog.accessItems.external')).toBeInTheDocument()
|
||||
expect(screen.getByText('app.accessControlDialog.accessItems.anyone')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,97 @@
|
||||
import type { AccessControlAccount, AccessControlGroup } from '@/models/access-control'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import useAccessControlStore from '@/context/access-control-store'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import SpecificGroupsOrMembers from '../specific-groups-or-members'
|
||||
|
||||
const mockUseAppWhiteListSubjects = vi.fn()
|
||||
|
||||
vi.mock('@/service/access-control', () => ({
|
||||
useAppWhiteListSubjects: (...args: unknown[]) => mockUseAppWhiteListSubjects(...args),
|
||||
}))
|
||||
|
||||
vi.mock('../add-member-or-group-pop', () => ({
|
||||
default: () => <div data-testid="add-member-or-group-dialog" />,
|
||||
}))
|
||||
|
||||
const createGroup = (overrides: Partial<AccessControlGroup> = {}): AccessControlGroup => ({
|
||||
id: 'group-1',
|
||||
name: 'Group One',
|
||||
groupSize: 5,
|
||||
...overrides,
|
||||
} as AccessControlGroup)
|
||||
|
||||
const createMember = (overrides: Partial<AccessControlAccount> = {}): AccessControlAccount => ({
|
||||
id: 'member-1',
|
||||
name: 'Member One',
|
||||
email: 'member@example.com',
|
||||
avatar: '',
|
||||
avatarUrl: '',
|
||||
...overrides,
|
||||
} as AccessControlAccount)
|
||||
|
||||
describe('SpecificGroupsOrMembers', () => {
|
||||
const baseGroup = createGroup()
|
||||
const baseMember = createMember()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
useAccessControlStore.setState({
|
||||
appId: '',
|
||||
specificGroups: [],
|
||||
specificMembers: [],
|
||||
currentMenu: AccessMode.SPECIFIC_GROUPS_MEMBERS,
|
||||
selectedGroupsForBreadcrumb: [],
|
||||
})
|
||||
mockUseAppWhiteListSubjects.mockReturnValue({
|
||||
isPending: false,
|
||||
data: {
|
||||
groups: [baseGroup],
|
||||
members: [baseMember],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should render the collapsed row when not in specific mode', () => {
|
||||
useAccessControlStore.setState({
|
||||
currentMenu: AccessMode.ORGANIZATION,
|
||||
})
|
||||
|
||||
render(<SpecificGroupsOrMembers />)
|
||||
|
||||
expect(screen.getByText('app.accessControlDialog.accessItems.specific')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('add-member-or-group-dialog')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show loading while whitelist subjects are pending', async () => {
|
||||
mockUseAppWhiteListSubjects.mockReturnValue({
|
||||
isPending: true,
|
||||
data: undefined,
|
||||
})
|
||||
|
||||
const { container } = render(<SpecificGroupsOrMembers />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(container.querySelector('.spin-animation')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should render fetched groups and members and support removal', async () => {
|
||||
useAccessControlStore.setState({ appId: 'app-1' })
|
||||
|
||||
render(<SpecificGroupsOrMembers />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(baseGroup.name)).toBeInTheDocument()
|
||||
expect(screen.getByText(baseMember.name)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const groupRemove = screen.getByText(baseGroup.name).closest('div')?.querySelector('.h-4.w-4.cursor-pointer') as HTMLElement
|
||||
fireEvent.click(groupRemove)
|
||||
expect(useAccessControlStore.getState().specificGroups).toEqual([])
|
||||
|
||||
const memberRemove = screen.getByText(baseMember.name).closest('div')?.querySelector('.h-4.w-4.cursor-pointer') as HTMLElement
|
||||
fireEvent.click(memberRemove)
|
||||
expect(useAccessControlStore.getState().specificMembers).toEqual([])
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,26 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import InputTypeIcon from '../input-type-icon'
|
||||
|
||||
const mockInputVarTypeIcon = vi.fn(({ type, className }: { type: InputVarType, className?: string }) => (
|
||||
<div data-testid="input-var-type-icon" data-type={type} className={className} />
|
||||
))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/input-var-type-icon', () => ({
|
||||
default: (props: { type: InputVarType, className?: string }) => mockInputVarTypeIcon(props),
|
||||
}))
|
||||
|
||||
describe('InputTypeIcon', () => {
|
||||
it('should map string variables to the workflow text-input icon', () => {
|
||||
render(<InputTypeIcon type="string" className="marker" />)
|
||||
|
||||
expect(screen.getByTestId('input-var-type-icon')).toHaveAttribute('data-type', InputVarType.textInput)
|
||||
expect(screen.getByTestId('input-var-type-icon')).toHaveClass('marker')
|
||||
})
|
||||
|
||||
it('should map select variables to the workflow select icon', () => {
|
||||
render(<InputTypeIcon type="select" className="marker" />)
|
||||
|
||||
expect(screen.getByTestId('input-var-type-icon')).toHaveAttribute('data-type', InputVarType.select)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,25 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import ModalFoot from '../modal-foot'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('ModalFoot', () => {
|
||||
it('should trigger cancel and confirm callbacks', () => {
|
||||
const onCancel = vi.fn()
|
||||
const onConfirm = vi.fn()
|
||||
|
||||
render(
|
||||
<ModalFoot onCancel={onCancel} onConfirm={onConfirm} />,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||
expect(onConfirm).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,22 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import SelectVarType from '../select-var-type'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('SelectVarType', () => {
|
||||
it('should open the menu and return the selected variable type', () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(<SelectVarType onChange={onChange} />)
|
||||
|
||||
fireEvent.click(screen.getByText('common.operation.add'))
|
||||
fireEvent.click(screen.getByText('appDebug.variableConfig.checkbox'))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('checkbox')
|
||||
expect(screen.queryByText('appDebug.variableConfig.checkbox')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,46 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import VarItem from '../var-item'
|
||||
|
||||
describe('VarItem', () => {
|
||||
it('should render variable metadata and allow editing', () => {
|
||||
const onEdit = vi.fn()
|
||||
const onRemove = vi.fn()
|
||||
const { container } = render(
|
||||
<VarItem
|
||||
canDrag
|
||||
name="api_key"
|
||||
label="API Key"
|
||||
required
|
||||
type="string"
|
||||
onEdit={onEdit}
|
||||
onRemove={onRemove}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTitle('api_key · API Key')).toBeInTheDocument()
|
||||
expect(screen.getByText('required')).toBeInTheDocument()
|
||||
|
||||
const editButton = container.querySelector('.mr-1.flex.h-6.w-6') as HTMLElement
|
||||
fireEvent.click(editButton)
|
||||
|
||||
expect(onEdit).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call remove when clicking the delete action', () => {
|
||||
const onRemove = vi.fn()
|
||||
render(
|
||||
<VarItem
|
||||
name="region"
|
||||
label="Region"
|
||||
required={false}
|
||||
type="select"
|
||||
onEdit={vi.fn()}
|
||||
onRemove={onRemove}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('var-item-delete-btn'))
|
||||
|
||||
expect(onRemove).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,23 @@
|
||||
import { jsonConfigPlaceHolder } from '../config'
|
||||
|
||||
describe('config modal placeholder config', () => {
|
||||
it('should contain a valid object schema example', () => {
|
||||
const parsed = JSON.parse(jsonConfigPlaceHolder) as {
|
||||
type: string
|
||||
properties: {
|
||||
foo: { type: string }
|
||||
bar: {
|
||||
type: string
|
||||
properties: {
|
||||
sub: { type: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(parsed.type).toBe('object')
|
||||
expect(parsed.properties.foo.type).toBe('string')
|
||||
expect(parsed.properties.bar.type).toBe('object')
|
||||
expect(parsed.properties.bar.properties.sub.type).toBe('number')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,31 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import Field from '../field'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('ConfigModal Field', () => {
|
||||
it('should render the title and children', () => {
|
||||
render(
|
||||
<Field title="Field title">
|
||||
<input aria-label="field-input" />
|
||||
</Field>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('Field title')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('field-input')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the optional hint when requested', () => {
|
||||
render(
|
||||
<Field title="Optional field" isOptional>
|
||||
<input aria-label="optional-field-input" />
|
||||
</Field>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(/\(appDebug\.variableConfig\.optional\)/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,80 @@
|
||||
import type { FeatureStoreState } from '@/app/components/base/features/store'
|
||||
import type { FileUpload } from '@/app/components/base/features/types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
import ParamConfigContent from '../param-config-content'
|
||||
|
||||
const mockUseFeatures = vi.fn()
|
||||
const mockUseFeaturesStore = vi.fn()
|
||||
const mockSetFeatures = vi.fn()
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/features/hooks', () => ({
|
||||
useFeatures: (selector: (state: FeatureStoreState) => unknown) => mockUseFeatures(selector),
|
||||
useFeaturesStore: () => mockUseFeaturesStore(),
|
||||
}))
|
||||
|
||||
const setupFeatureStore = (fileOverrides: Partial<FileUpload> = {}) => {
|
||||
const file: FileUpload = {
|
||||
enabled: true,
|
||||
allowed_file_types: [],
|
||||
allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
||||
number_limits: 3,
|
||||
image: {
|
||||
enabled: true,
|
||||
detail: Resolution.low,
|
||||
number_limits: 3,
|
||||
transfer_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
||||
},
|
||||
...fileOverrides,
|
||||
}
|
||||
const featureStoreState = {
|
||||
features: { file },
|
||||
setFeatures: mockSetFeatures,
|
||||
showFeaturesModal: false,
|
||||
setShowFeaturesModal: vi.fn(),
|
||||
} as unknown as FeatureStoreState
|
||||
|
||||
mockUseFeatures.mockImplementation(selector => selector(featureStoreState))
|
||||
mockUseFeaturesStore.mockReturnValue({
|
||||
getState: () => featureStoreState,
|
||||
})
|
||||
}
|
||||
|
||||
const getUpdatedFile = () => {
|
||||
expect(mockSetFeatures).toHaveBeenCalled()
|
||||
return mockSetFeatures.mock.calls.at(-1)?.[0].file as FileUpload
|
||||
}
|
||||
|
||||
describe('ParamConfigContent', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setupFeatureStore()
|
||||
})
|
||||
|
||||
it('should update the image resolution', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<ParamConfigContent />)
|
||||
|
||||
await user.click(screen.getByText('appDebug.vision.visionSettings.high'))
|
||||
|
||||
expect(getUpdatedFile().image?.detail).toBe(Resolution.high)
|
||||
})
|
||||
|
||||
it('should update upload methods and upload limit', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<ParamConfigContent />)
|
||||
|
||||
await user.click(screen.getByText('appDebug.vision.visionSettings.localUpload'))
|
||||
expect(getUpdatedFile().allowed_file_upload_methods).toEqual([TransferMethod.local_file])
|
||||
|
||||
fireEvent.change(screen.getByRole('textbox'), { target: { value: '5' } })
|
||||
expect(getUpdatedFile().number_limits).toBe(5)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,64 @@
|
||||
import type { FeatureStoreState } from '@/app/components/base/features/store'
|
||||
import type { FileUpload } from '@/app/components/base/features/types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
import ParamConfig from '../param-config'
|
||||
|
||||
const mockUseFeatures = vi.fn()
|
||||
const mockUseFeaturesStore = vi.fn()
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/features/hooks', () => ({
|
||||
useFeatures: (selector: (state: FeatureStoreState) => unknown) => mockUseFeatures(selector),
|
||||
useFeaturesStore: () => mockUseFeaturesStore(),
|
||||
}))
|
||||
|
||||
const setupFeatureStore = (fileOverrides: Partial<FileUpload> = {}) => {
|
||||
const file: FileUpload = {
|
||||
enabled: true,
|
||||
allowed_file_types: [],
|
||||
allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
||||
number_limits: 3,
|
||||
image: {
|
||||
enabled: true,
|
||||
detail: Resolution.low,
|
||||
number_limits: 3,
|
||||
transfer_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
||||
},
|
||||
...fileOverrides,
|
||||
}
|
||||
const featureStoreState = {
|
||||
features: { file },
|
||||
setFeatures: vi.fn(),
|
||||
showFeaturesModal: false,
|
||||
setShowFeaturesModal: vi.fn(),
|
||||
} as unknown as FeatureStoreState
|
||||
mockUseFeatures.mockImplementation(selector => selector(featureStoreState))
|
||||
mockUseFeaturesStore.mockReturnValue({
|
||||
getState: () => featureStoreState,
|
||||
})
|
||||
}
|
||||
|
||||
describe('ParamConfig', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setupFeatureStore()
|
||||
})
|
||||
|
||||
it('should toggle the settings panel when clicking the trigger', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<ParamConfig />)
|
||||
|
||||
expect(screen.queryByText('appDebug.vision.visionSettings.title')).not.toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'appDebug.voice.settings' }))
|
||||
|
||||
expect(await screen.findByText('appDebug.vision.visionSettings.title')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,28 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import PromptToast from '../prompt-toast'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('PromptToast', () => {
|
||||
it('should render the note title and markdown message', () => {
|
||||
render(<PromptToast message="Prompt body" />)
|
||||
|
||||
expect(screen.getByText('appDebug.generate.optimizationNote')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('markdown-body')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should collapse and expand the markdown content', () => {
|
||||
const { container } = render(<PromptToast message="Prompt body" />)
|
||||
|
||||
const toggle = container.querySelector('.cursor-pointer') as HTMLElement
|
||||
fireEvent.click(toggle)
|
||||
expect(screen.queryByTestId('markdown-body')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(toggle)
|
||||
expect(screen.getByTestId('markdown-body')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,16 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import ResPlaceholder from '../res-placeholder'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('ResPlaceholder', () => {
|
||||
it('should render the placeholder copy', () => {
|
||||
render(<ResPlaceholder />)
|
||||
|
||||
expect(screen.getByText('appDebug.generate.newNoDataLine1')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,47 @@
|
||||
import type { GenRes } from '@/service/debug'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import useGenData from '../use-gen-data'
|
||||
|
||||
vi.mock('ahooks', async (importOriginal) => {
|
||||
const React = await import('react')
|
||||
const actual = await importOriginal<typeof import('ahooks')>()
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useSessionStorageState: <T>(_key: string, options: { defaultValue: T }) => {
|
||||
const [value, setValue] = React.useState(options.defaultValue)
|
||||
return [value, setValue] as const
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe('useGenData', () => {
|
||||
it('should start with an empty version list', () => {
|
||||
const { result } = renderHook(() => useGenData({ storageKey: 'prompt' }))
|
||||
|
||||
expect(result.current.versions).toEqual([])
|
||||
expect(result.current.currentVersionIndex).toBe(0)
|
||||
expect(result.current.current).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should append versions and keep the latest one selected', () => {
|
||||
const versionOne = { modified: 'first version' } as GenRes
|
||||
const versionTwo = { modified: 'second version' } as GenRes
|
||||
const { result } = renderHook(() => useGenData({ storageKey: 'prompt' }))
|
||||
|
||||
act(() => {
|
||||
result.current.addVersion(versionOne)
|
||||
})
|
||||
|
||||
expect(result.current.versions).toEqual([versionOne])
|
||||
expect(result.current.current).toEqual(versionOne)
|
||||
|
||||
act(() => {
|
||||
result.current.addVersion(versionTwo)
|
||||
})
|
||||
|
||||
expect(result.current.versions).toEqual([versionOne, versionTwo])
|
||||
expect(result.current.currentVersionIndex).toBe(1)
|
||||
expect(result.current.current).toEqual(versionTwo)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,38 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { useDebugWithMultipleModelContext } from '../context'
|
||||
import { DebugWithMultipleModelContextProvider } from '../context-provider'
|
||||
|
||||
const ContextConsumer = () => {
|
||||
const value = useDebugWithMultipleModelContext()
|
||||
return (
|
||||
<div>
|
||||
<div>{value.multipleModelConfigs.length}</div>
|
||||
<button onClick={() => value.onMultipleModelConfigsChange(true, value.multipleModelConfigs)}>change-multiple</button>
|
||||
<button onClick={() => value.onDebugWithMultipleModelChange(value.multipleModelConfigs[0])}>change-single</button>
|
||||
<div>{String(value.checkCanSend?.())}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
describe('DebugWithMultipleModelContextProvider', () => {
|
||||
it('should expose the provided context value to descendants', () => {
|
||||
const onMultipleModelConfigsChange = vi.fn()
|
||||
const onDebugWithMultipleModelChange = vi.fn()
|
||||
const checkCanSend = vi.fn(() => true)
|
||||
const multipleModelConfigs = [{ model: 'gpt-4o' }] as unknown as []
|
||||
|
||||
render(
|
||||
<DebugWithMultipleModelContextProvider
|
||||
multipleModelConfigs={multipleModelConfigs}
|
||||
onMultipleModelConfigsChange={onMultipleModelConfigsChange}
|
||||
onDebugWithMultipleModelChange={onDebugWithMultipleModelChange}
|
||||
checkCanSend={checkCanSend}
|
||||
>
|
||||
<ContextConsumer />
|
||||
</DebugWithMultipleModelContextProvider>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('1')).toBeInTheDocument()
|
||||
expect(screen.getByText('true')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,103 @@
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { AppCardAccessControlSection, AppCardOperations, createAppCardOperations } from '../app-card-sections'
|
||||
|
||||
describe('app-card-sections', () => {
|
||||
const t = (key: string) => key
|
||||
|
||||
it('should build operations with the expected disabled state', () => {
|
||||
const onLaunch = vi.fn()
|
||||
const operations = createAppCardOperations({
|
||||
operationKeys: ['launch', 'settings'],
|
||||
t: t as never,
|
||||
runningStatus: false,
|
||||
triggerModeDisabled: false,
|
||||
onLaunch,
|
||||
onEmbedded: vi.fn(),
|
||||
onCustomize: vi.fn(),
|
||||
onSettings: vi.fn(),
|
||||
onDevelop: vi.fn(),
|
||||
})
|
||||
|
||||
expect(operations[0]).toMatchObject({
|
||||
key: 'launch',
|
||||
disabled: true,
|
||||
label: 'overview.appInfo.launch',
|
||||
})
|
||||
expect(operations[1]).toMatchObject({
|
||||
key: 'settings',
|
||||
disabled: false,
|
||||
label: 'overview.appInfo.settings.entry',
|
||||
})
|
||||
})
|
||||
|
||||
it('should render the access-control section and call onClick', () => {
|
||||
const onClick = vi.fn()
|
||||
render(
|
||||
<AppCardAccessControlSection
|
||||
t={t as never}
|
||||
appDetail={{ access_mode: AccessMode.SPECIFIC_GROUPS_MEMBERS } as AppDetailResponse}
|
||||
isAppAccessSet={false}
|
||||
onClick={onClick}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('publishApp.notSet'))
|
||||
|
||||
expect(screen.getByText('accessControlDialog.accessItems.specific')).toBeInTheDocument()
|
||||
expect(onClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should render operation buttons and execute enabled actions', () => {
|
||||
const onLaunch = vi.fn()
|
||||
const operations = createAppCardOperations({
|
||||
operationKeys: ['launch', 'embedded'],
|
||||
t: t as never,
|
||||
runningStatus: true,
|
||||
triggerModeDisabled: false,
|
||||
onLaunch,
|
||||
onEmbedded: vi.fn(),
|
||||
onCustomize: vi.fn(),
|
||||
onSettings: vi.fn(),
|
||||
onDevelop: vi.fn(),
|
||||
})
|
||||
|
||||
render(
|
||||
<AppCardOperations
|
||||
t={t as never}
|
||||
operations={operations}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /overview\.appInfo\.launch/i }))
|
||||
|
||||
expect(onLaunch).toHaveBeenCalledTimes(1)
|
||||
expect(screen.getByRole('button', { name: /overview\.appInfo\.embedded\.entry/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep customize available for web app cards that are not completion or workflow apps', () => {
|
||||
const operations = createAppCardOperations({
|
||||
operationKeys: ['customize'],
|
||||
t: t as never,
|
||||
runningStatus: true,
|
||||
triggerModeDisabled: false,
|
||||
onLaunch: vi.fn(),
|
||||
onEmbedded: vi.fn(),
|
||||
onCustomize: vi.fn(),
|
||||
onSettings: vi.fn(),
|
||||
onDevelop: vi.fn(),
|
||||
})
|
||||
|
||||
render(
|
||||
<AppCardOperations
|
||||
t={t as never}
|
||||
operations={operations}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('overview.appInfo.customize.entry')).toBeInTheDocument()
|
||||
expect(AppModeEnum.CHAT).toBe('chat')
|
||||
})
|
||||
})
|
||||
107
web/app/components/app/overview/__tests__/app-card-utils.spec.ts
Normal file
107
web/app/components/app/overview/__tests__/app-card-utils.spec.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import type { AppDetailResponse } from '@/models/app'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { AccessMode } from '@/models/access-control'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { getAppCardDisplayState, getAppCardOperationKeys, hasWorkflowStartNode, isAppAccessConfigured } from '../app-card-utils'
|
||||
|
||||
describe('app-card-utils', () => {
|
||||
const baseAppInfo = {
|
||||
id: 'app-1',
|
||||
mode: AppModeEnum.CHAT,
|
||||
enable_site: true,
|
||||
enable_api: false,
|
||||
access_mode: AccessMode.PUBLIC,
|
||||
api_base_url: 'https://api.example.com',
|
||||
site: {
|
||||
app_base_url: 'https://example.com',
|
||||
access_token: 'token-1',
|
||||
},
|
||||
} as AppDetailResponse
|
||||
|
||||
it('should detect whether the workflow includes a start node', () => {
|
||||
expect(hasWorkflowStartNode({
|
||||
graph: {
|
||||
nodes: [{ data: { type: BlockEnum.Start } }],
|
||||
},
|
||||
})).toBe(true)
|
||||
|
||||
expect(hasWorkflowStartNode({
|
||||
graph: {
|
||||
nodes: [{ data: { type: BlockEnum.Answer } }],
|
||||
},
|
||||
})).toBe(false)
|
||||
})
|
||||
|
||||
it('should build the display state for a published web app', () => {
|
||||
const state = getAppCardDisplayState({
|
||||
appInfo: baseAppInfo,
|
||||
cardType: 'webapp',
|
||||
currentWorkflow: null,
|
||||
isCurrentWorkspaceEditor: true,
|
||||
isCurrentWorkspaceManager: true,
|
||||
})
|
||||
|
||||
expect(state.isApp).toBe(true)
|
||||
expect(state.appMode).toBe(AppModeEnum.CHAT)
|
||||
expect(state.runningStatus).toBe(true)
|
||||
expect(state.accessibleUrl).toBe(`https://example.com${basePath}/chat/token-1`)
|
||||
})
|
||||
|
||||
it('should disable workflow cards without a graph or start node', () => {
|
||||
const unpublishedState = getAppCardDisplayState({
|
||||
appInfo: { ...baseAppInfo, mode: AppModeEnum.WORKFLOW },
|
||||
cardType: 'webapp',
|
||||
currentWorkflow: null,
|
||||
isCurrentWorkspaceEditor: true,
|
||||
isCurrentWorkspaceManager: true,
|
||||
})
|
||||
expect(unpublishedState.appUnpublished).toBe(true)
|
||||
expect(unpublishedState.toggleDisabled).toBe(true)
|
||||
|
||||
const missingStartState = getAppCardDisplayState({
|
||||
appInfo: { ...baseAppInfo, mode: AppModeEnum.WORKFLOW },
|
||||
cardType: 'webapp',
|
||||
currentWorkflow: {
|
||||
graph: {
|
||||
nodes: [{ data: { type: BlockEnum.Answer } }],
|
||||
},
|
||||
},
|
||||
isCurrentWorkspaceEditor: true,
|
||||
isCurrentWorkspaceManager: true,
|
||||
})
|
||||
expect(missingStartState.missingStartNode).toBe(true)
|
||||
expect(missingStartState.runningStatus).toBe(false)
|
||||
})
|
||||
|
||||
it('should require specific access subjects only for the specific access mode', () => {
|
||||
expect(isAppAccessConfigured(
|
||||
{ ...baseAppInfo, access_mode: AccessMode.PUBLIC },
|
||||
{ groups: [], members: [] },
|
||||
)).toBe(true)
|
||||
|
||||
expect(isAppAccessConfigured(
|
||||
{ ...baseAppInfo, access_mode: AccessMode.SPECIFIC_GROUPS_MEMBERS },
|
||||
{ groups: [], members: [] },
|
||||
)).toBe(false)
|
||||
|
||||
expect(isAppAccessConfigured(
|
||||
{ ...baseAppInfo, access_mode: AccessMode.SPECIFIC_GROUPS_MEMBERS },
|
||||
{ groups: [{ id: 'group-1' }], members: [] },
|
||||
)).toBe(true)
|
||||
})
|
||||
|
||||
it('should derive operation keys for api and webapp cards', () => {
|
||||
expect(getAppCardOperationKeys({
|
||||
cardType: 'api',
|
||||
appMode: AppModeEnum.COMPLETION,
|
||||
isCurrentWorkspaceEditor: true,
|
||||
})).toEqual(['develop'])
|
||||
|
||||
expect(getAppCardOperationKeys({
|
||||
cardType: 'webapp',
|
||||
appMode: AppModeEnum.CHAT,
|
||||
isCurrentWorkspaceEditor: false,
|
||||
})).toEqual(['launch', 'embedded', 'customize'])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user