mirror of
https://github.com/langgenius/dify.git
synced 2026-02-24 01:45:13 +00:00
test(web): add members-page account-setting specs and improve coverage (#32311)
This commit is contained in:
@@ -32,10 +32,10 @@ describe('YearAndMonthPicker Options', () => {
|
|||||||
it('should render year options', () => {
|
it('should render year options', () => {
|
||||||
const props = createOptionsProps()
|
const props = createOptionsProps()
|
||||||
|
|
||||||
render(<Options {...props} />)
|
const { container } = render(<Options {...props} />)
|
||||||
|
|
||||||
const allItems = screen.getAllByRole('listitem')
|
const yearList = container.querySelectorAll('ul')[1]
|
||||||
expect(allItems).toHaveLength(212)
|
expect(yearList?.children).toHaveLength(200)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import type { AppContextValue } from '@/context/app-context'
|
||||||
|
import type { ICurrentWorkspace } from '@/models/common'
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
|
import { useAppContext } from '@/context/app-context'
|
||||||
|
import { updateWorkspaceInfo } from '@/service/common'
|
||||||
|
import EditWorkspaceModal from './index'
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context')
|
||||||
|
vi.mock('@/service/common')
|
||||||
|
|
||||||
|
describe('EditWorkspaceModal', () => {
|
||||||
|
const mockOnCancel = vi.fn()
|
||||||
|
const mockNotify = vi.fn()
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
|
||||||
|
vi.mocked(useAppContext).mockReturnValue({
|
||||||
|
currentWorkspace: { name: 'Test Workspace' } as ICurrentWorkspace,
|
||||||
|
isCurrentWorkspaceOwner: true,
|
||||||
|
} as unknown as AppContextValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderModal = () => render(
|
||||||
|
<ToastContext.Provider value={{ notify: mockNotify, close: vi.fn() }}>
|
||||||
|
<EditWorkspaceModal onCancel={mockOnCancel} />
|
||||||
|
</ToastContext.Provider>,
|
||||||
|
)
|
||||||
|
|
||||||
|
it('should show current workspace name in the input', async () => {
|
||||||
|
renderModal()
|
||||||
|
|
||||||
|
expect(await screen.findByDisplayValue('Test Workspace')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should let user edit workspace name', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
renderModal()
|
||||||
|
|
||||||
|
const input = screen.getByPlaceholderText(/account\.workspaceNamePlaceholder/i)
|
||||||
|
await user.clear(input)
|
||||||
|
await user.type(input, 'New Workspace Name')
|
||||||
|
|
||||||
|
expect(input).toHaveValue('New Workspace Name')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should submit update when confirming as owner', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
const mockAssign = vi.fn()
|
||||||
|
vi.stubGlobal('location', { ...window.location, assign: mockAssign })
|
||||||
|
vi.mocked(updateWorkspaceInfo).mockResolvedValue({} as ICurrentWorkspace)
|
||||||
|
|
||||||
|
renderModal()
|
||||||
|
|
||||||
|
const input = screen.getByPlaceholderText(/account\.workspaceNamePlaceholder/i)
|
||||||
|
await user.clear(input)
|
||||||
|
await user.type(input, 'Renamed Workspace')
|
||||||
|
await user.click(screen.getByRole('button', { name: /operation\.confirm/i }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(updateWorkspaceInfo).toHaveBeenCalledWith({
|
||||||
|
url: '/workspaces/info',
|
||||||
|
body: { name: 'Renamed Workspace' },
|
||||||
|
})
|
||||||
|
expect(mockAssign).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show error toast when update fails', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
vi.mocked(updateWorkspaceInfo).mockRejectedValue(new Error('update failed'))
|
||||||
|
|
||||||
|
renderModal()
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: /operation\.confirm/i }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
type: 'error',
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should disable confirm button for non-owners', async () => {
|
||||||
|
vi.mocked(useAppContext).mockReturnValue({
|
||||||
|
currentWorkspace: { name: 'Test Workspace' } as ICurrentWorkspace,
|
||||||
|
isCurrentWorkspaceOwner: false,
|
||||||
|
} as unknown as AppContextValue)
|
||||||
|
|
||||||
|
renderModal()
|
||||||
|
|
||||||
|
expect(await screen.findByRole('button', { name: /operation\.confirm/i })).toBeDisabled()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import type { AppContextValue } from '@/context/app-context'
|
||||||
|
import type { ICurrentWorkspace, Member } from '@/models/common'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
|
||||||
|
import { useAppContext } from '@/context/app-context'
|
||||||
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
|
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||||
|
import { useMembers } from '@/service/use-common'
|
||||||
|
import MembersPage from './index'
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context')
|
||||||
|
vi.mock('@/context/global-public-context')
|
||||||
|
vi.mock('@/context/provider-context')
|
||||||
|
vi.mock('@/hooks/use-format-time-from-now')
|
||||||
|
vi.mock('@/service/use-common')
|
||||||
|
|
||||||
|
vi.mock('./edit-workspace-modal', () => ({
|
||||||
|
default: ({ onCancel }: { onCancel: () => void }) => (
|
||||||
|
<div>
|
||||||
|
<div>Edit Workspace Modal</div>
|
||||||
|
<button onClick={onCancel}>Close Edit Workspace</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
vi.mock('./invite-button', () => ({
|
||||||
|
default: ({ onClick, disabled }: { onClick: () => void, disabled: boolean }) => (
|
||||||
|
<button onClick={onClick} disabled={disabled}>Invite</button>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
vi.mock('./invite-modal', () => ({
|
||||||
|
default: ({ onCancel, onSend }: { onCancel: () => void, onSend: (results: Array<{ email: string, status: 'success', url: string }>) => void }) => (
|
||||||
|
<div>
|
||||||
|
<div>Invite Modal</div>
|
||||||
|
<button onClick={onCancel}>Close Invite Modal</button>
|
||||||
|
<button onClick={() => onSend([{ email: 'sent@example.com', status: 'success', url: 'http://invite/link' }])}>Send Invite Results</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
vi.mock('./invited-modal', () => ({
|
||||||
|
default: ({ onCancel }: { onCancel: () => void }) => (
|
||||||
|
<div>
|
||||||
|
<div>Invited Modal</div>
|
||||||
|
<button onClick={onCancel}>Close Invited Modal</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
vi.mock('./operation', () => ({
|
||||||
|
default: () => <div>Member Operation</div>,
|
||||||
|
}))
|
||||||
|
vi.mock('./operation/transfer-ownership', () => ({
|
||||||
|
default: ({ onOperate }: { onOperate: () => void }) => <button onClick={onOperate}>Transfer ownership</button>,
|
||||||
|
}))
|
||||||
|
vi.mock('./transfer-ownership-modal', () => ({
|
||||||
|
default: ({ onClose }: { onClose: () => void }) => (
|
||||||
|
<div>
|
||||||
|
<div>Transfer Ownership Modal</div>
|
||||||
|
<button onClick={onClose}>Close Transfer Modal</button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('MembersPage', () => {
|
||||||
|
const mockRefetch = vi.fn()
|
||||||
|
const mockFormatTimeFromNow = vi.fn(() => 'just now')
|
||||||
|
|
||||||
|
const mockAccounts: Member[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'Owner User',
|
||||||
|
email: 'owner@example.com',
|
||||||
|
avatar: '',
|
||||||
|
avatar_url: '',
|
||||||
|
role: 'owner',
|
||||||
|
last_active_at: '1731000000',
|
||||||
|
last_login_at: '1731000000',
|
||||||
|
created_at: '1731000000',
|
||||||
|
status: 'active',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Admin User',
|
||||||
|
email: 'admin@example.com',
|
||||||
|
avatar: '',
|
||||||
|
avatar_url: '',
|
||||||
|
role: 'admin',
|
||||||
|
last_active_at: '1731000000',
|
||||||
|
last_login_at: '1731000000',
|
||||||
|
created_at: '1731000000',
|
||||||
|
status: 'active',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
|
||||||
|
vi.mocked(useAppContext).mockReturnValue({
|
||||||
|
userProfile: { email: 'owner@example.com' },
|
||||||
|
currentWorkspace: { name: 'Test Workspace', role: 'owner' } as ICurrentWorkspace,
|
||||||
|
isCurrentWorkspaceOwner: true,
|
||||||
|
isCurrentWorkspaceManager: true,
|
||||||
|
} as unknown as AppContextValue)
|
||||||
|
|
||||||
|
vi.mocked(useMembers).mockReturnValue({
|
||||||
|
data: { accounts: mockAccounts },
|
||||||
|
refetch: mockRefetch,
|
||||||
|
} as unknown as ReturnType<typeof useMembers>)
|
||||||
|
|
||||||
|
vi.mocked(useGlobalPublicStore).mockImplementation(selector => selector({
|
||||||
|
systemFeatures: { is_email_setup: true },
|
||||||
|
} as unknown as Parameters<typeof selector>[0]))
|
||||||
|
|
||||||
|
vi.mocked(useProviderContext).mockReturnValue(createMockProviderContextValue({
|
||||||
|
enableBilling: false,
|
||||||
|
isAllowTransferWorkspace: true,
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mocked(useFormatTimeFromNow).mockReturnValue({
|
||||||
|
formatTimeFromNow: mockFormatTimeFromNow,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render workspace and member information', () => {
|
||||||
|
render(<MembersPage />)
|
||||||
|
|
||||||
|
expect(screen.getByText('Test Workspace')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Owner User')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Admin User')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should open and close invite modal', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
render(<MembersPage />)
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: /invite/i }))
|
||||||
|
expect(screen.getByText('Invite Modal')).toBeInTheDocument()
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Close Invite Modal' }))
|
||||||
|
expect(screen.queryByText('Invite Modal')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should open invited modal after invite results are sent', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
render(<MembersPage />)
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: /invite/i }))
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Send Invite Results' }))
|
||||||
|
|
||||||
|
expect(screen.getByText('Invited Modal')).toBeInTheDocument()
|
||||||
|
expect(mockRefetch).toHaveBeenCalled()
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'Close Invited Modal' }))
|
||||||
|
expect(screen.queryByText('Invited Modal')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should open transfer ownership modal when transfer action is used', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
render(<MembersPage />)
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: /transfer ownership/i }))
|
||||||
|
expect(screen.getByText('Transfer Ownership Modal')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show non-interactive owner role when transfer ownership is not allowed', () => {
|
||||||
|
vi.mocked(useProviderContext).mockReturnValue(createMockProviderContextValue({
|
||||||
|
enableBilling: false,
|
||||||
|
isAllowTransferWorkspace: false,
|
||||||
|
}))
|
||||||
|
|
||||||
|
render(<MembersPage />)
|
||||||
|
|
||||||
|
expect(screen.getByText('common.members.owner')).toBeInTheDocument()
|
||||||
|
expect(screen.queryByRole('button', { name: /transfer ownership/i })).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should hide manager controls for non-owner non-manager users', () => {
|
||||||
|
vi.mocked(useAppContext).mockReturnValue({
|
||||||
|
userProfile: { email: 'admin@example.com' },
|
||||||
|
currentWorkspace: { name: 'Test Workspace', role: 'admin' } as ICurrentWorkspace,
|
||||||
|
isCurrentWorkspaceOwner: false,
|
||||||
|
isCurrentWorkspaceManager: false,
|
||||||
|
} as unknown as AppContextValue)
|
||||||
|
|
||||||
|
render(<MembersPage />)
|
||||||
|
|
||||||
|
expect(screen.queryByRole('button', { name: /invite/i })).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('Transfer ownership')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import type { AppContextValue } from '@/context/app-context'
|
||||||
|
import type { ICurrentWorkspace } from '@/models/common'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
import { useAppContext } from '@/context/app-context'
|
||||||
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
|
import { useWorkspacePermissions } from '@/service/use-workspace'
|
||||||
|
import InviteButton from './invite-button'
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context')
|
||||||
|
vi.mock('@/context/global-public-context')
|
||||||
|
vi.mock('@/service/use-workspace')
|
||||||
|
|
||||||
|
describe('InviteButton', () => {
|
||||||
|
const setupMocks = ({
|
||||||
|
brandingEnabled,
|
||||||
|
isFetching,
|
||||||
|
allowInvite,
|
||||||
|
}: {
|
||||||
|
brandingEnabled: boolean
|
||||||
|
isFetching: boolean
|
||||||
|
allowInvite?: boolean
|
||||||
|
}) => {
|
||||||
|
vi.mocked(useGlobalPublicStore).mockImplementation(selector => selector({
|
||||||
|
systemFeatures: { branding: { enabled: brandingEnabled } },
|
||||||
|
} as unknown as Parameters<typeof selector>[0]))
|
||||||
|
vi.mocked(useWorkspacePermissions).mockReturnValue({
|
||||||
|
data: allowInvite === undefined ? null : { allow_member_invite: allowInvite },
|
||||||
|
isFetching,
|
||||||
|
} as unknown as ReturnType<typeof useWorkspacePermissions>)
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
vi.mocked(useAppContext).mockReturnValue({
|
||||||
|
currentWorkspace: { id: 'workspace-id' } as ICurrentWorkspace,
|
||||||
|
} as unknown as AppContextValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show invite button when branding is disabled', () => {
|
||||||
|
setupMocks({ brandingEnabled: false, isFetching: false })
|
||||||
|
|
||||||
|
render(<InviteButton />)
|
||||||
|
|
||||||
|
expect(screen.getByRole('button', { name: /members\.invite/i })).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show loading status while permissions are loading', () => {
|
||||||
|
setupMocks({ brandingEnabled: true, isFetching: true })
|
||||||
|
|
||||||
|
render(<InviteButton />)
|
||||||
|
|
||||||
|
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should hide invite button when permission is denied', () => {
|
||||||
|
setupMocks({ brandingEnabled: true, isFetching: false, allowInvite: false })
|
||||||
|
|
||||||
|
render(<InviteButton />)
|
||||||
|
|
||||||
|
expect(screen.queryByRole('button', { name: /members\.invite/i })).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show invite button when permission is granted', () => {
|
||||||
|
setupMocks({ brandingEnabled: true, isFetching: false, allowInvite: true })
|
||||||
|
|
||||||
|
render(<InviteButton />)
|
||||||
|
|
||||||
|
expect(screen.getByRole('button', { name: /members\.invite/i })).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import type { InvitationResponse } from '@/models/common'
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
|
import { useProviderContextSelector } from '@/context/provider-context'
|
||||||
|
import { inviteMember } from '@/service/common'
|
||||||
|
import InviteModal from './index'
|
||||||
|
|
||||||
|
vi.mock('@/context/provider-context', () => ({
|
||||||
|
useProviderContextSelector: vi.fn(),
|
||||||
|
useProviderContext: vi.fn(() => ({
|
||||||
|
datasetOperatorEnabled: true,
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
vi.mock('@/service/common')
|
||||||
|
vi.mock('@/context/i18n', () => ({
|
||||||
|
useLocale: () => 'en-US',
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('InviteModal', () => {
|
||||||
|
const mockOnCancel = vi.fn()
|
||||||
|
const mockOnSend = vi.fn()
|
||||||
|
const mockRefreshLicenseLimit = vi.fn()
|
||||||
|
const mockNotify = vi.fn()
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
|
||||||
|
vi.mocked(useProviderContextSelector).mockImplementation(selector => selector({
|
||||||
|
licenseLimit: { workspace_members: { size: 5, limit: 10 } },
|
||||||
|
refreshLicenseLimit: mockRefreshLicenseLimit,
|
||||||
|
} as unknown as Parameters<typeof selector>[0]))
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderModal = (isEmailSetup = true) => render(
|
||||||
|
<ToastContext.Provider value={{ notify: mockNotify, close: vi.fn() }}>
|
||||||
|
<InviteModal isEmailSetup={isEmailSetup} onCancel={mockOnCancel} onSend={mockOnSend} />
|
||||||
|
</ToastContext.Provider>,
|
||||||
|
)
|
||||||
|
|
||||||
|
it('should render invite modal content', async () => {
|
||||||
|
renderModal()
|
||||||
|
|
||||||
|
expect(await screen.findByText(/members\.inviteTeamMember$/i)).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('button', { name: /members\.sendInvite/i })).toBeDisabled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show warning when email service is not configured', async () => {
|
||||||
|
renderModal(false)
|
||||||
|
|
||||||
|
expect(await screen.findByText(/members\.emailNotSetup$/i)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should enable send button after entering an email', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
renderModal()
|
||||||
|
|
||||||
|
const input = screen.getByRole('textbox')
|
||||||
|
await user.type(input, 'user@example.com{enter}')
|
||||||
|
|
||||||
|
expect(screen.getByRole('button', { name: /members\.sendInvite/i })).toBeEnabled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not close modal when invite request fails', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
vi.mocked(inviteMember).mockRejectedValue(new Error('request failed'))
|
||||||
|
|
||||||
|
renderModal()
|
||||||
|
|
||||||
|
await user.type(screen.getByRole('textbox'), 'user@example.com{enter}')
|
||||||
|
await user.click(screen.getByRole('button', { name: /members\.sendInvite/i }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(inviteMember).toHaveBeenCalled()
|
||||||
|
expect(mockOnCancel).not.toHaveBeenCalled()
|
||||||
|
expect(mockOnSend).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should send invites and close modal on successful submission', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
vi.mocked(inviteMember).mockResolvedValue({
|
||||||
|
result: 'success',
|
||||||
|
invitation_results: [],
|
||||||
|
} as InvitationResponse)
|
||||||
|
|
||||||
|
renderModal()
|
||||||
|
|
||||||
|
const input = screen.getByRole('textbox')
|
||||||
|
await user.type(input, 'user@example.com{enter}')
|
||||||
|
await user.click(screen.getByRole('button', { name: /members\.sendInvite/i }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(inviteMember).toHaveBeenCalled()
|
||||||
|
expect(mockRefreshLicenseLimit).toHaveBeenCalled()
|
||||||
|
expect(mockOnCancel).toHaveBeenCalled()
|
||||||
|
expect(mockOnSend).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should keep send button disabled when license limit is exceeded', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
vi.mocked(useProviderContextSelector).mockImplementation(selector => selector({
|
||||||
|
licenseLimit: { workspace_members: { size: 10, limit: 10 } },
|
||||||
|
refreshLicenseLimit: mockRefreshLicenseLimit,
|
||||||
|
} as unknown as Parameters<typeof selector>[0]))
|
||||||
|
|
||||||
|
renderModal()
|
||||||
|
|
||||||
|
const input = screen.getByRole('textbox')
|
||||||
|
await user.type(input, 'user@example.com{enter}')
|
||||||
|
|
||||||
|
expect(screen.getByRole('button', { name: /members\.sendInvite/i })).toBeDisabled()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
import { useProviderContext } from '@/context/provider-context'
|
||||||
|
import RoleSelector from './role-selector'
|
||||||
|
|
||||||
|
vi.mock('@/context/provider-context')
|
||||||
|
|
||||||
|
type WrapperProps = {
|
||||||
|
initialRole?: 'normal' | 'editor' | 'admin' | 'dataset_operator'
|
||||||
|
}
|
||||||
|
|
||||||
|
const RoleSelectorWrapper = ({ initialRole = 'normal' }: WrapperProps) => {
|
||||||
|
const [role, setRole] = useState<'normal' | 'editor' | 'admin' | 'dataset_operator'>(initialRole)
|
||||||
|
return <RoleSelector value={role} onChange={setRole} />
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('RoleSelector', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
vi.mocked(useProviderContext).mockReturnValue({
|
||||||
|
datasetOperatorEnabled: true,
|
||||||
|
} as unknown as ReturnType<typeof useProviderContext>)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show current role in trigger text', () => {
|
||||||
|
render(<RoleSelectorWrapper initialRole="admin" />)
|
||||||
|
|
||||||
|
expect(screen.getByText(/members\.invitedAsRole/i)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
'common.members.admin',
|
||||||
|
'common.members.editor',
|
||||||
|
'common.members.datasetOperator',
|
||||||
|
])('should update selected role after user chooses %s', async (nextRoleLabel) => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
render(<RoleSelectorWrapper initialRole="normal" />)
|
||||||
|
|
||||||
|
await user.click(screen.getByText(/members\.invitedAsRole/i))
|
||||||
|
await user.click(screen.getByText(nextRoleLabel))
|
||||||
|
|
||||||
|
expect(screen.getByText(new RegExp(nextRoleLabel.replace('.', '\\.'), 'i'))).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should hide dataset operator option when feature is disabled', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
vi.mocked(useProviderContext).mockReturnValue({
|
||||||
|
datasetOperatorEnabled: false,
|
||||||
|
} as unknown as ReturnType<typeof useProviderContext>)
|
||||||
|
|
||||||
|
render(<RoleSelectorWrapper />)
|
||||||
|
|
||||||
|
await user.click(screen.getByText(/members\.invitedAsRole/i))
|
||||||
|
|
||||||
|
expect(screen.queryByText('common.members.datasetOperator')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import type { InvitationResult } from '@/models/common'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import InvitedModal from './index'
|
||||||
|
|
||||||
|
vi.mock('@/config', () => ({
|
||||||
|
IS_CE_EDITION: true,
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('InvitedModal', () => {
|
||||||
|
const mockOnCancel = vi.fn()
|
||||||
|
const results: InvitationResult[] = [
|
||||||
|
{ email: 'success@example.com', status: 'success', url: 'http://invite.com/1' },
|
||||||
|
{ email: 'failed@example.com', status: 'failed', message: 'Error msg' },
|
||||||
|
]
|
||||||
|
|
||||||
|
it('should show success and failed invitation sections', async () => {
|
||||||
|
render(<InvitedModal invitationResults={results} onCancel={mockOnCancel} />)
|
||||||
|
|
||||||
|
expect(await screen.findByText(/members\.invitationSent$/i)).toBeInTheDocument()
|
||||||
|
expect(await screen.findByText(/members\.invitationLink/i)).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('http://invite.com/1')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('failed@example.com')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import InvitationLink from './invitation-link'
|
||||||
|
|
||||||
|
describe('InvitationLink', () => {
|
||||||
|
const value = { email: 'test@example.com', status: 'success' as const, url: '/invite/123' }
|
||||||
|
|
||||||
|
it('should render invitation url and keep it visible after click', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
render(<InvitationLink value={value} />)
|
||||||
|
|
||||||
|
const url = screen.getByText('/invite/123')
|
||||||
|
await user.click(url)
|
||||||
|
|
||||||
|
expect(url).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should keep link visible after copy feedback timeout passes', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
render(<InvitationLink value={value} />)
|
||||||
|
|
||||||
|
await user.click(screen.getByText('/invite/123'))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('/invite/123')).toBeInTheDocument()
|
||||||
|
}, { timeout: 1500 })
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Member } from '@/models/common'
|
import type { Member } from '@/models/common'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
import { vi } from 'vitest'
|
import { vi } from 'vitest'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
import Operation from './index'
|
import Operation from './index'
|
||||||
@@ -55,20 +56,45 @@ describe('Operation', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('shows dataset operator option when the feature flag is enabled', async () => {
|
it('shows dataset operator option when the feature flag is enabled', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
mockUseProviderContext.mockReturnValue({ datasetOperatorEnabled: true })
|
mockUseProviderContext.mockReturnValue({ datasetOperatorEnabled: true })
|
||||||
renderOperation()
|
renderOperation()
|
||||||
|
|
||||||
fireEvent.click(screen.getByText('common.members.editor'))
|
await user.click(screen.getByText('common.members.editor'))
|
||||||
|
|
||||||
expect(await screen.findByText('common.members.datasetOperator')).toBeInTheDocument()
|
expect(await screen.findByText('common.members.datasetOperator')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('shows owner-allowed role options for admin operators', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
renderOperation({}, 'admin')
|
||||||
|
|
||||||
|
await user.click(screen.getByText('common.members.editor'))
|
||||||
|
|
||||||
|
expect(screen.queryByText('common.members.admin')).not.toBeInTheDocument()
|
||||||
|
expect(screen.getByText('common.members.normal')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not show role options for unsupported operators', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
renderOperation({}, 'normal')
|
||||||
|
|
||||||
|
await user.click(screen.getByText('common.members.editor'))
|
||||||
|
|
||||||
|
expect(screen.queryByText('common.members.normal')).not.toBeInTheDocument()
|
||||||
|
expect(screen.getByText('common.members.removeFromTeam')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
it('calls updateMemberRole and onOperate when selecting another role', async () => {
|
it('calls updateMemberRole and onOperate when selecting another role', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
const onOperate = vi.fn()
|
const onOperate = vi.fn()
|
||||||
renderOperation({}, 'owner', onOperate)
|
renderOperation({}, 'owner', onOperate)
|
||||||
|
|
||||||
fireEvent.click(screen.getByText('common.members.editor'))
|
await user.click(screen.getByText('common.members.editor'))
|
||||||
fireEvent.click(await screen.findByText('common.members.normal'))
|
await user.click(await screen.findByText('common.members.normal'))
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockUpdateMemberRole).toHaveBeenCalled()
|
expect(mockUpdateMemberRole).toHaveBeenCalled()
|
||||||
@@ -77,11 +103,12 @@ describe('Operation', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('calls deleteMemberOrCancelInvitation when removing the member', async () => {
|
it('calls deleteMemberOrCancelInvitation when removing the member', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
const onOperate = vi.fn()
|
const onOperate = vi.fn()
|
||||||
renderOperation({}, 'owner', onOperate)
|
renderOperation({}, 'owner', onOperate)
|
||||||
|
|
||||||
fireEvent.click(screen.getByText('common.members.editor'))
|
await user.click(screen.getByText('common.members.editor'))
|
||||||
fireEvent.click(await screen.findByText('common.members.removeFromTeam'))
|
await user.click(await screen.findByText('common.members.removeFromTeam'))
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockDeleteMemberOrCancelInvitation).toHaveBeenCalled()
|
expect(mockDeleteMemberOrCancelInvitation).toHaveBeenCalled()
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import type { AppContextValue } from '@/context/app-context'
|
||||||
|
import type { ICurrentWorkspace } from '@/models/common'
|
||||||
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
import { useAppContext } from '@/context/app-context'
|
||||||
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
|
import { useWorkspacePermissions } from '@/service/use-workspace'
|
||||||
|
import TransferOwnership from './transfer-ownership'
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context')
|
||||||
|
vi.mock('@/context/global-public-context')
|
||||||
|
vi.mock('@/service/use-workspace')
|
||||||
|
|
||||||
|
describe('TransferOwnership', () => {
|
||||||
|
const setupMocks = ({
|
||||||
|
brandingEnabled,
|
||||||
|
isFetching,
|
||||||
|
allowOwnerTransfer,
|
||||||
|
}: {
|
||||||
|
brandingEnabled: boolean
|
||||||
|
isFetching: boolean
|
||||||
|
allowOwnerTransfer?: boolean
|
||||||
|
}) => {
|
||||||
|
vi.mocked(useGlobalPublicStore).mockImplementation(selector => selector({
|
||||||
|
systemFeatures: { branding: { enabled: brandingEnabled } },
|
||||||
|
} as unknown as Parameters<typeof selector>[0]))
|
||||||
|
vi.mocked(useWorkspacePermissions).mockReturnValue({
|
||||||
|
data: allowOwnerTransfer === undefined ? null : { allow_owner_transfer: allowOwnerTransfer },
|
||||||
|
isFetching,
|
||||||
|
} as unknown as ReturnType<typeof useWorkspacePermissions>)
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
vi.mocked(useAppContext).mockReturnValue({
|
||||||
|
currentWorkspace: { id: 'workspace-id' } as ICurrentWorkspace,
|
||||||
|
} as unknown as AppContextValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show loading status while permissions are loading', () => {
|
||||||
|
setupMocks({ brandingEnabled: true, isFetching: true })
|
||||||
|
|
||||||
|
render(<TransferOwnership onOperate={vi.fn()} />)
|
||||||
|
|
||||||
|
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show owner text without transfer menu when transfer is forbidden', () => {
|
||||||
|
setupMocks({ brandingEnabled: true, isFetching: false, allowOwnerTransfer: false })
|
||||||
|
|
||||||
|
render(<TransferOwnership onOperate={vi.fn()} />)
|
||||||
|
|
||||||
|
expect(screen.getByText(/members\.owner/i)).toBeInTheDocument()
|
||||||
|
expect(screen.queryByText(/members\.transferOwnership/i)).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should open transfer dialog when transfer option is selected', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
const onOperate = vi.fn()
|
||||||
|
|
||||||
|
setupMocks({ brandingEnabled: true, isFetching: false, allowOwnerTransfer: true })
|
||||||
|
|
||||||
|
render(<TransferOwnership onOperate={onOperate} />)
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: /members\.owner/i }))
|
||||||
|
const transferOptionText = await screen.findByText(/members\.transferOwnership/i)
|
||||||
|
const transferOption = transferOptionText.closest('div.cursor-pointer')
|
||||||
|
if (!transferOption)
|
||||||
|
throw new Error('Transfer option container not found')
|
||||||
|
fireEvent.click(transferOption)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onOperate).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should allow transfer menu when branding is disabled', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
setupMocks({ brandingEnabled: false, isFetching: false })
|
||||||
|
|
||||||
|
render(<TransferOwnership onOperate={vi.fn()} />)
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: /members\.owner/i }))
|
||||||
|
|
||||||
|
expect(screen.getByText(/members\.transferOwnership/i)).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import type { AppContextValue } from '@/context/app-context'
|
||||||
|
import type { ICurrentWorkspace } from '@/models/common'
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
|
import { useAppContext } from '@/context/app-context'
|
||||||
|
import { ownershipTransfer, sendOwnerEmail, verifyOwnerEmail } from '@/service/common'
|
||||||
|
import TransferOwnershipModal from './index'
|
||||||
|
|
||||||
|
vi.mock('@/context/app-context')
|
||||||
|
vi.mock('@/service/common')
|
||||||
|
|
||||||
|
vi.mock('./member-selector', () => ({
|
||||||
|
default: ({ onSelect }: { onSelect: (id: string) => void }) => (
|
||||||
|
<button onClick={() => onSelect('new-owner-id')}>Select member</button>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('TransferOwnershipModal', () => {
|
||||||
|
const mockOnClose = vi.fn()
|
||||||
|
const mockNotify = vi.fn()
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
vi.spyOn(globalThis, 'setInterval').mockImplementation(() => 0 as unknown as ReturnType<typeof setInterval>)
|
||||||
|
vi.spyOn(globalThis, 'clearInterval').mockImplementation(() => {})
|
||||||
|
|
||||||
|
vi.mocked(useAppContext).mockReturnValue({
|
||||||
|
currentWorkspace: { name: 'Test Workspace' } as ICurrentWorkspace,
|
||||||
|
userProfile: { email: 'owner@example.com', id: 'owner-id' },
|
||||||
|
} as unknown as AppContextValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderModal = () => render(
|
||||||
|
<ToastContext.Provider value={{ notify: mockNotify, close: vi.fn() }}>
|
||||||
|
<TransferOwnershipModal show onClose={mockOnClose} />
|
||||||
|
</ToastContext.Provider>,
|
||||||
|
)
|
||||||
|
|
||||||
|
const mockEmailVerification = ({
|
||||||
|
isValid = true,
|
||||||
|
token = 'final-token',
|
||||||
|
}: {
|
||||||
|
isValid?: boolean
|
||||||
|
token?: string
|
||||||
|
} = {}) => {
|
||||||
|
vi.mocked(sendOwnerEmail).mockResolvedValue({
|
||||||
|
data: 'step-token',
|
||||||
|
result: 'success',
|
||||||
|
} as Awaited<ReturnType<typeof sendOwnerEmail>>)
|
||||||
|
vi.mocked(verifyOwnerEmail).mockResolvedValue({
|
||||||
|
is_valid: isValid,
|
||||||
|
token,
|
||||||
|
result: 'success',
|
||||||
|
} as Awaited<ReturnType<typeof verifyOwnerEmail>>)
|
||||||
|
}
|
||||||
|
|
||||||
|
const goToTransferStep = async (user: ReturnType<typeof userEvent.setup>) => {
|
||||||
|
await user.click(screen.getByRole('button', { name: /members\.transferModal\.sendVerifyCode/i }))
|
||||||
|
await user.type(screen.getByPlaceholderText(/members\.transferModal\.codePlaceholder/i), '123456')
|
||||||
|
await user.click(screen.getByRole('button', { name: /members\.transferModal\.continue/i }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectNewOwnerAndSubmit = async (user: ReturnType<typeof userEvent.setup>) => {
|
||||||
|
await user.click(screen.getByRole('button', { name: /select member/i }))
|
||||||
|
await user.click(screen.getByRole('button', { name: /members\.transferModal\.transfer$/i }))
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should complete ownership transfer flow through all steps', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
mockEmailVerification()
|
||||||
|
vi.mocked(ownershipTransfer).mockResolvedValue({
|
||||||
|
result: 'success',
|
||||||
|
} as Awaited<ReturnType<typeof ownershipTransfer>>)
|
||||||
|
|
||||||
|
const mockReload = vi.fn()
|
||||||
|
vi.stubGlobal('location', { ...window.location, reload: mockReload })
|
||||||
|
|
||||||
|
renderModal()
|
||||||
|
|
||||||
|
await goToTransferStep(user)
|
||||||
|
|
||||||
|
expect(await screen.findByText(/members\.transferModal\.transferLabel/i)).toBeInTheDocument()
|
||||||
|
|
||||||
|
await selectNewOwnerAndSubmit(user)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(ownershipTransfer).toHaveBeenCalledWith('new-owner-id', { token: 'final-token' })
|
||||||
|
expect(mockReload).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
}, 15000)
|
||||||
|
|
||||||
|
it('should show error when email verification returns invalid code', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
mockEmailVerification({ isValid: false, token: 'step-token' })
|
||||||
|
|
||||||
|
renderModal()
|
||||||
|
|
||||||
|
await goToTransferStep(user)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
type: 'error',
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show error when sending verification email fails', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
vi.mocked(sendOwnerEmail).mockRejectedValue(new Error('network error'))
|
||||||
|
|
||||||
|
renderModal()
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: /members\.transferModal\.sendVerifyCode/i }))
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
type: 'error',
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show error when ownership transfer fails', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
mockEmailVerification()
|
||||||
|
vi.mocked(ownershipTransfer).mockRejectedValue(new Error('transfer failed'))
|
||||||
|
|
||||||
|
renderModal()
|
||||||
|
|
||||||
|
await goToTransferStep(user)
|
||||||
|
await selectNewOwnerAndSubmit(user)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
type: 'error',
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import type { Member } from '@/models/common'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
import { useMembers } from '@/service/use-common'
|
||||||
|
import MemberSelector from './member-selector'
|
||||||
|
|
||||||
|
vi.mock('@/service/use-common')
|
||||||
|
|
||||||
|
const MemberSelectorHarness = ({ initialValue = '', exclude = [] as string[] }: { initialValue?: string, exclude?: string[] }) => {
|
||||||
|
const [selected, setSelected] = useState<string>(initialValue)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MemberSelector value={selected} onSelect={setSelected} exclude={exclude} />
|
||||||
|
{selected && (
|
||||||
|
<div>
|
||||||
|
Selected:
|
||||||
|
{' '}
|
||||||
|
{selected}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('MemberSelector', () => {
|
||||||
|
const mockMembers = [
|
||||||
|
{ id: '1', name: 'User 1', email: 'user1@example.com', role: 'admin' },
|
||||||
|
{ id: '2', name: 'User 2', email: 'user2@example.com', role: 'normal' },
|
||||||
|
] as Member[]
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
vi.mocked(useMembers).mockReturnValue({
|
||||||
|
data: { accounts: mockMembers },
|
||||||
|
} as unknown as ReturnType<typeof useMembers>)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show member options when selector is opened', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
render(<MemberSelectorHarness />)
|
||||||
|
|
||||||
|
await user.click(screen.getByText(/members\.transferModal\.transferPlaceholder/i))
|
||||||
|
|
||||||
|
expect(screen.getByPlaceholderText(/common\.operation\.search/i)).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('User 1')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('User 2')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should filter displayed members by search term', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
render(<MemberSelectorHarness />)
|
||||||
|
|
||||||
|
await user.click(screen.getByText(/members\.transferModal\.transferPlaceholder/i))
|
||||||
|
await user.type(screen.getByPlaceholderText(/common\.operation\.search/i), 'User 2')
|
||||||
|
|
||||||
|
expect(screen.queryByText('User 1')).not.toBeInTheDocument()
|
||||||
|
expect(screen.getByText('User 2')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show selected member after clicking an option', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
render(<MemberSelectorHarness />)
|
||||||
|
|
||||||
|
await user.click(screen.getByText(/members\.transferModal\.transferPlaceholder/i))
|
||||||
|
await user.click(screen.getByText('User 1'))
|
||||||
|
|
||||||
|
expect(screen.getByText('Selected: 1')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show selected value details when an initial value is provided', () => {
|
||||||
|
render(<MemberSelectorHarness initialValue="2" />)
|
||||||
|
|
||||||
|
expect(screen.getByText('User 2')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('user2@example.com')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should hide excluded members from options', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
render(<MemberSelectorHarness exclude={['1']} />)
|
||||||
|
|
||||||
|
await user.click(screen.getByText(/members\.transferModal\.transferPlaceholder/i))
|
||||||
|
|
||||||
|
expect(screen.queryByText('User 1')).not.toBeInTheDocument()
|
||||||
|
expect(screen.getByText('User 2')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render empty options when member data is unavailable', async () => {
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
vi.mocked(useMembers).mockReturnValue({
|
||||||
|
data: undefined,
|
||||||
|
} as unknown as ReturnType<typeof useMembers>)
|
||||||
|
|
||||||
|
render(<MemberSelectorHarness />)
|
||||||
|
|
||||||
|
await user.click(screen.getByText(/members\.transferModal\.transferPlaceholder/i))
|
||||||
|
|
||||||
|
expect(screen.queryByText('User 1')).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('User 2')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user