mirror of
https://github.com/langgenius/dify.git
synced 2026-11-10 21:35:45 +08:00
test(web): replace fabricated form, model and feedback controls (#45220)
This commit is contained in:
1 parent
5336037aa9
commit
b89f43a66a
4 files changed
+180
-207
No files matched your search
+62
-56
@@ -3,56 +3,26 @@ import type {
|
||||
DatasourceProviderAuthResponse,
|
||||
ProviderConfig,
|
||||
} from '@dify/contracts/api/console/auth/types.gen'
|
||||
import type {
|
||||
AddApiKeyButtonProps,
|
||||
AddOAuthButtonProps,
|
||||
PluginPayload,
|
||||
} from '@/app/components/plugins/plugin-auth/types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import type { PluginPayload } from '@/app/components/plugins/plugin-auth/types'
|
||||
import { screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import * as React from 'react'
|
||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
import { AuthCategory } from '@/app/components/plugins/plugin-auth/types'
|
||||
import { renderWithConsoleQuery as render } from '@/test/console/query-data'
|
||||
import Configure from '../configure'
|
||||
|
||||
// Mock plugin auth components to isolate the unit test for Configure.
|
||||
vi.mock('@/app/components/plugins/plugin-auth', () => ({
|
||||
AddApiKeyButton: vi.fn(
|
||||
({ onUpdate, disabled, buttonText }: AddApiKeyButtonProps & { onUpdate: () => void }) => (
|
||||
<button onClick={onUpdate} disabled={disabled}>
|
||||
{buttonText}
|
||||
</button>
|
||||
),
|
||||
),
|
||||
AddOAuthButton: vi.fn(
|
||||
({ onUpdate, disabled, buttonText, renderTrigger }: AddOAuthButtonProps) => {
|
||||
const [permissionOpen, setPermissionOpen] = React.useState(false)
|
||||
const handleSaveAndAuthorize = () => {
|
||||
onUpdate?.()
|
||||
setPermissionOpen(true)
|
||||
}
|
||||
const trigger = (
|
||||
<button onClick={handleSaveAndAuthorize} disabled={disabled}>
|
||||
{buttonText}
|
||||
</button>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderTrigger
|
||||
? renderTrigger({
|
||||
disabled,
|
||||
isConfigured: false,
|
||||
onClick: handleSaveAndAuthorize,
|
||||
trigger,
|
||||
})
|
||||
: trigger}
|
||||
{permissionOpen && <div role="dialog" aria-label="Who can use" />}
|
||||
</>
|
||||
)
|
||||
},
|
||||
),
|
||||
}))
|
||||
const service = vi.hoisted(() => ({ addCredential: vi.fn(), saveOAuthClient: vi.fn() }))
|
||||
vi.mock('@/service/console', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/service/console')>()
|
||||
const { withConsoleResponses } = await import('@/test/console/service-responses')
|
||||
return {
|
||||
...actual,
|
||||
consoleQuery: withConsoleResponses(actual.consoleQuery, {
|
||||
'auth.plugin.datasource.byProviderId.post': service.addCredential,
|
||||
'auth.plugin.datasource.byProviderId.customClient.post': service.saveOAuthClient,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
describe('Configure Component', () => {
|
||||
const mockOnUpdate = vi.fn()
|
||||
@@ -95,7 +65,8 @@ describe('Configure Component', () => {
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
service.addCredential.mockResolvedValue({ result: 'success' })
|
||||
service.saveOAuthClient.mockResolvedValue({ result: 'success' })
|
||||
})
|
||||
|
||||
describe('Open State Management', () => {
|
||||
@@ -133,7 +104,9 @@ describe('Configure Component', () => {
|
||||
await user.click(screen.getByRole('button', { name: /dataSource.configure/i }))
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.auth.addApi' })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'plugin.auth.addOAuth' })).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'plugin.auth.setupOAuth' }),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render AddOAuthButton when oauth_schema with client_schema is non-empty', async () => {
|
||||
@@ -148,7 +121,7 @@ describe('Configure Component', () => {
|
||||
render(<Configure item={itemWithOAuth} pluginPayload={mockPluginPayload} />)
|
||||
await user.click(screen.getByRole('button', { name: /dataSource.configure/i }))
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.auth.addOAuth' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'plugin.auth.setupOAuth' })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'plugin.auth.addApi' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
@@ -166,7 +139,7 @@ describe('Configure Component', () => {
|
||||
await user.click(screen.getByRole('button', { name: /dataSource.configure/i }))
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.auth.addApi' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'plugin.auth.addOAuth' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'plugin.auth.setupOAuth' })).toBeInTheDocument()
|
||||
expect(screen.getByText('OR')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -189,11 +162,26 @@ describe('Configure Component', () => {
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /dataSource.configure/i }))
|
||||
await user.click(screen.getByRole('button', { name: 'plugin.auth.addOAuth' }))
|
||||
await user.click(screen.getByRole('button', { name: 'plugin.auth.setupOAuth' }))
|
||||
const settings = screen.getByRole('dialog', { name: 'plugin.auth.oauthClientSettings' })
|
||||
expect(mockOnUpdate).not.toHaveBeenCalled()
|
||||
await user.type(within(settings).getByRole('textbox', { name: /API Key/ }), 'client-secret')
|
||||
await user.click(within(settings).getByRole('button', { name: 'plugin.auth.saveAndAuth' }))
|
||||
|
||||
expect(
|
||||
await screen.findByRole('dialog', { name: 'plugin.auth.whoCanUse' }),
|
||||
).toBeInTheDocument()
|
||||
expect(service.saveOAuthClient).toHaveBeenCalledExactlyOnceWith({
|
||||
params: { provider_id: 'test-provider' },
|
||||
body: { client_params: { api_key: 'client-secret' }, enable_oauth_custom_client: true },
|
||||
})
|
||||
expect(mockOnUpdate).toHaveBeenCalledOnce()
|
||||
expect(screen.queryByRole('button', { name: 'plugin.auth.addOAuth' })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('dialog', { name: 'Who can use' })).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'plugin.auth.setupOAuth' }),
|
||||
).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('dialog', { name: 'plugin.auth.oauthClientSettings' }),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onUpdate and close the portal when an update is triggered', async () => {
|
||||
@@ -212,7 +200,19 @@ describe('Configure Component', () => {
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /dataSource.configure/i }))
|
||||
await user.click(screen.getByRole('button', { name: 'plugin.auth.addApi' }))
|
||||
expect(mockOnUpdate).not.toHaveBeenCalled()
|
||||
await user.type(screen.getByRole('textbox', { name: /API Key/ }), 'datasource-key')
|
||||
await user.type(
|
||||
screen.getByRole('textbox', { name: 'plugin.auth.authorizationName' }),
|
||||
'Team source',
|
||||
)
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument())
|
||||
|
||||
expect(service.addCredential).toHaveBeenCalledExactlyOnceWith({
|
||||
params: { provider_id: 'test-provider' },
|
||||
body: { credentials: { api_key: 'datasource-key' }, name: 'Team source' },
|
||||
})
|
||||
expect(mockOnUpdate).toHaveBeenCalledTimes(1)
|
||||
expect(screen.queryByRole('button', { name: 'plugin.auth.addApi' })).not.toBeInTheDocument()
|
||||
})
|
||||
@@ -233,7 +233,7 @@ describe('Configure Component', () => {
|
||||
await user.click(screen.getByRole('button', { name: /dataSource.configure/i }))
|
||||
|
||||
expect(screen.getByRole('button', { name: 'plugin.auth.addApi' })).toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: 'plugin.auth.addOAuth' })).toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: 'plugin.auth.setupOAuth' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should handle edge cases for missing, empty, or partial item data', async () => {
|
||||
@@ -243,7 +243,9 @@ describe('Configure Component', () => {
|
||||
)
|
||||
await user.click(screen.getByRole('button', { name: /dataSource.configure/i }))
|
||||
expect(screen.queryByRole('button', { name: 'plugin.auth.addApi' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'plugin.auth.addOAuth' })).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'plugin.auth.setupOAuth' }),
|
||||
).not.toBeInTheDocument()
|
||||
|
||||
const itemEmpty: DatasourceProviderAuthResponse = {
|
||||
...mockItemBase,
|
||||
@@ -254,7 +256,9 @@ describe('Configure Component', () => {
|
||||
rerender(<Configure item={itemEmpty} pluginPayload={mockPluginPayload} />)
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'plugin.auth.addApi' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'plugin.auth.addOAuth' })).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'plugin.auth.setupOAuth' }),
|
||||
).not.toBeInTheDocument()
|
||||
|
||||
const itemPartialOAuth: DatasourceProviderAuthResponse = {
|
||||
...mockItemBase,
|
||||
@@ -265,7 +269,9 @@ describe('Configure Component', () => {
|
||||
|
||||
rerender(<Configure item={itemPartialOAuth} pluginPayload={mockPluginPayload} />)
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'plugin.auth.addOAuth' })).not.toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'plugin.auth.setupOAuth' }),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
-56
@@ -1,5 +1,3 @@
|
||||
import type { HumanInputFieldValue } from '@/app/components/base/chat/chat/answer/human-input-content/field-renderer'
|
||||
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import type { HumanInputFormData } from '@/types/workflow'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
@@ -10,56 +8,6 @@ import SingleRunForm from '../single-run-form'
|
||||
const mockToastError = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/app/notifications', () => ({ toast: { error: mockToastError } }))
|
||||
|
||||
vi.mock('@/app/components/base/chat/chat/answer/human-input-content/content-item', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
content,
|
||||
formInputFields,
|
||||
inputs,
|
||||
onInputChange,
|
||||
}: {
|
||||
content: string
|
||||
formInputFields: FormInputItem[]
|
||||
inputs: Record<string, HumanInputFieldValue>
|
||||
onInputChange: (name: string, value: HumanInputFieldValue) => void
|
||||
}) => {
|
||||
const fieldName = /\{\{#\$output\.([^#]+)#\}\}/.exec(content)?.[1]
|
||||
if (!fieldName) return <div>{content}</div>
|
||||
|
||||
const field = formInputFields.find((field) => field.output_variable_name === fieldName)
|
||||
if (!field) return null
|
||||
|
||||
if (field.type === 'select') {
|
||||
return (
|
||||
<select
|
||||
aria-label={fieldName}
|
||||
value={typeof inputs[fieldName] === 'string' ? inputs[fieldName] : ''}
|
||||
onChange={(event) => onInputChange(fieldName, event.target.value)}
|
||||
>
|
||||
<option value="">Select</option>
|
||||
{field.option_source.value.map((option) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.type === 'paragraph') {
|
||||
return (
|
||||
<textarea
|
||||
aria-label={fieldName}
|
||||
value={typeof inputs[fieldName] === 'string' ? inputs[fieldName] : ''}
|
||||
onChange={(event) => onInputChange(fieldName, event.target.value)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <div>{fieldName}</div>
|
||||
},
|
||||
}))
|
||||
|
||||
const createFormData = (overrides: Partial<HumanInputFormData> = {}): HumanInputFormData => ({
|
||||
form_id: 'form-1',
|
||||
node_id: 'human-1',
|
||||
@@ -91,10 +39,6 @@ const createFormData = (overrides: Partial<HumanInputFormData> = {}): HumanInput
|
||||
})
|
||||
|
||||
describe('SingleRunForm', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('restores the submit action and keeps entered values after a server failure', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSubmit = vi
|
||||
|
||||
+57
-28
@@ -1,6 +1,7 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import { screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useState } from 'react'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import {
|
||||
createCatalogModelFixture,
|
||||
@@ -9,29 +10,31 @@ import {
|
||||
seedModelProviderQueries,
|
||||
} from '@/test/console/model-provider'
|
||||
import { createConsoleQueryClient, renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import { createNuqsTestWrapper } from '@/test/nuqs-testing'
|
||||
import RecallSettings from '../recall-settings'
|
||||
|
||||
// The model selector has its own suite; this one only needs the selected value and a choice.
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/model-selector', () => ({
|
||||
ModelSelector: (props: {
|
||||
onValueChange: (value: { model: string; provider: string }) => void
|
||||
value?: { model: string; provider: string }
|
||||
}) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.onValueChange({ provider: 'custom/provider', model: 'custom-rerank' })}
|
||||
>
|
||||
reranker {props.value ? `${props.value.provider}/${props.value.model}` : 'none'}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
function renderSettings(props: Partial<ComponentProps<typeof RecallSettings>> = {}) {
|
||||
const queryClient = createConsoleQueryClient()
|
||||
seedModelProviderQueries(queryClient, [
|
||||
createModelCatalogFixture({
|
||||
provider: 'system/provider',
|
||||
models: [createCatalogModelFixture({ model: 'system-rerank', model_type: 'rerank' })],
|
||||
models: [
|
||||
createCatalogModelFixture({
|
||||
model: 'system-rerank',
|
||||
label: { en_US: 'system-rerank' },
|
||||
model_type: 'rerank',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
createModelCatalogFixture({
|
||||
provider: 'custom/provider',
|
||||
models: [
|
||||
createCatalogModelFixture({
|
||||
model: 'custom-rerank',
|
||||
label: { en_US: 'custom-rerank' },
|
||||
model_type: 'rerank',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
])
|
||||
queryClient.setQueryData(
|
||||
@@ -46,15 +49,37 @@ function renderSettings(props: Partial<ComponentProps<typeof RecallSettings>> =
|
||||
}),
|
||||
},
|
||||
)
|
||||
const { wrapper: UrlProvider } = createNuqsTestWrapper()
|
||||
function Settings() {
|
||||
const [rerankingModel, setRerankingModel] = useState(props.rerankingModel)
|
||||
const [topK, setTopK] = useState(10)
|
||||
const [scoreThreshold, setScoreThreshold] = useState(props.scoreThreshold)
|
||||
return (
|
||||
<RecallSettings
|
||||
onModeChange={vi.fn()}
|
||||
{...props}
|
||||
topK={topK}
|
||||
scoreThreshold={scoreThreshold}
|
||||
rerankingModel={rerankingModel}
|
||||
onRerankingModelChange={(value) => {
|
||||
setRerankingModel(value)
|
||||
props.onRerankingModelChange?.(value)
|
||||
}}
|
||||
onScoreThresholdChange={(value) => {
|
||||
setScoreThreshold(value)
|
||||
props.onScoreThresholdChange?.(value)
|
||||
}}
|
||||
onTopKChange={(value) => {
|
||||
setTopK(value)
|
||||
props.onTopKChange?.(value)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return renderWithConsoleQuery(
|
||||
<RecallSettings
|
||||
topK={10}
|
||||
onModeChange={vi.fn()}
|
||||
onRerankingModelChange={vi.fn()}
|
||||
onScoreThresholdChange={vi.fn()}
|
||||
onTopKChange={vi.fn()}
|
||||
{...props}
|
||||
/>,
|
||||
<UrlProvider>
|
||||
<Settings />
|
||||
</UrlProvider>,
|
||||
{ queryClient },
|
||||
)
|
||||
}
|
||||
@@ -67,7 +92,9 @@ describe('RecallSettings', () => {
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' }))
|
||||
expect(screen.getByText('modelProvider.modelProvider.defaultConfig')).toBeInTheDocument()
|
||||
await user.click(screen.getByRole('button', { name: 'reranker system/provider/system-rerank' }))
|
||||
await user.click(screen.getByRole('button', { name: /system-rerank/ }))
|
||||
await user.click(await screen.findByRole('button', { name: /custom-rerank/ }))
|
||||
expect(screen.getByRole('button', { name: /custom-rerank/ })).toBeInTheDocument()
|
||||
expect(onRerankingModelChange).toHaveBeenCalledWith({
|
||||
provider: 'custom/provider',
|
||||
model: 'custom-rerank',
|
||||
@@ -85,6 +112,7 @@ describe('RecallSettings', () => {
|
||||
})
|
||||
slider.focus()
|
||||
await user.keyboard('{End}')
|
||||
expect(slider).toHaveValue('100')
|
||||
expect(onTopKChange).toHaveBeenLastCalledWith(100)
|
||||
})
|
||||
|
||||
@@ -95,6 +123,7 @@ describe('RecallSettings', () => {
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' }))
|
||||
await user.click(screen.getByRole('switch'))
|
||||
expect(screen.getByRole('switch')).toBeChecked()
|
||||
expect(onScoreThresholdChange).toHaveBeenLastCalledWith(0.5)
|
||||
})
|
||||
|
||||
@@ -120,10 +149,10 @@ describe('RecallSettings', () => {
|
||||
})
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' }))
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'reranker custom/provider/custom-rerank' }),
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /custom-rerank/ })).toBeInTheDocument()
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.reset' }))
|
||||
expect(screen.getByRole('button', { name: /system-rerank/ })).toBeInTheDocument()
|
||||
expect(screen.getByText('modelProvider.modelProvider.defaultConfig')).toBeInTheDocument()
|
||||
expect(onRerankingModelChange).toHaveBeenCalledWith(undefined)
|
||||
})
|
||||
})
|
||||
@@ -2,45 +2,20 @@ import type {
|
||||
AgentLogConversationItemResponse,
|
||||
AgentLogMessageListResponse,
|
||||
} from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { IChatItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { OnFeedback } from '@/app/components/base/chat/types'
|
||||
import { Drawer } from '@langgenius/dify-ui/drawer'
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { consoleQuery } from '@/service/console'
|
||||
import { QueryClientTestProvider } from '@/test/console/query-provider'
|
||||
import { AgentLogDetailPanel } from '../components/log-detail-panel'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
chatProps: vi.fn(),
|
||||
feedbackMutationFn: vi.fn(),
|
||||
messagesQueryFn: vi.fn(),
|
||||
toastError: vi.fn(),
|
||||
toastSuccess: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/chat/chat', () => ({
|
||||
default: ({
|
||||
chatList,
|
||||
config,
|
||||
onFeedback,
|
||||
}: {
|
||||
chatList: IChatItem[]
|
||||
config?: { supportFeedback?: boolean }
|
||||
onFeedback?: OnFeedback
|
||||
}) => {
|
||||
mocks.chatProps({ chatList, config, onFeedback })
|
||||
return (
|
||||
<button
|
||||
onClick={() => void onFeedback?.('message-1', { rating: 'like' }).catch(() => undefined)}
|
||||
>
|
||||
submit-feedback
|
||||
</button>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/notifications', () => ({
|
||||
toast: {
|
||||
error: mocks.toastError,
|
||||
@@ -61,6 +36,10 @@ vi.mock('@/service/console', async (importOriginal) => {
|
||||
...actual,
|
||||
consoleQuery: withConsoleResponses(actual.consoleQuery, {
|
||||
'agent.byAgentId.feedbacks.post': mocks.feedbackMutationFn,
|
||||
'agent.byAgentId.traceRuns.byMessageId.get': async () => ({ status: 'not_collected' }),
|
||||
'apps.byAppId.traceRuns.bySourceKind.bySourceId.get': async () => ({
|
||||
status: 'not_collected',
|
||||
}),
|
||||
'agent.byAgentId.logs.byConversationId.messages.get': mocks.messagesQueryFn,
|
||||
}),
|
||||
}
|
||||
@@ -124,12 +103,15 @@ const webappMessages: AgentLogMessageListResponse = {
|
||||
total: 1,
|
||||
}
|
||||
|
||||
function renderPanel(log: AgentLogConversationItemResponse, messages: AgentLogMessageListResponse) {
|
||||
mocks.messagesQueryFn.mockResolvedValue(messages)
|
||||
function renderPanel(
|
||||
log: AgentLogConversationItemResponse,
|
||||
messages: AgentLogMessageListResponse,
|
||||
refreshedMessages = messages,
|
||||
) {
|
||||
mocks.messagesQueryFn.mockResolvedValueOnce(messages).mockResolvedValue(refreshedMessages)
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
})
|
||||
const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries')
|
||||
|
||||
render(
|
||||
<QueryClientTestProvider queryClient={queryClient}>
|
||||
@@ -138,8 +120,6 @@ function renderPanel(log: AgentLogConversationItemResponse, messages: AgentLogMe
|
||||
</Drawer>
|
||||
</QueryClientTestProvider>,
|
||||
)
|
||||
|
||||
return { invalidateQueries }
|
||||
}
|
||||
|
||||
describe('AgentLogDetailPanel', () => {
|
||||
@@ -148,54 +128,71 @@ describe('AgentLogDetailPanel', () => {
|
||||
mocks.feedbackMutationFn.mockResolvedValue({ result: 'success' })
|
||||
})
|
||||
|
||||
it('maps user and admin feedback and submits operator feedback for webapp messages', async () => {
|
||||
it('maps user and admin feedback and removes operator feedback for webapp messages', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { invalidateQueries } = renderPanel(webappLog, webappMessages)
|
||||
renderPanel(webappLog, webappMessages, {
|
||||
...webappMessages,
|
||||
data: [
|
||||
{
|
||||
...webappMessages.data[0]!,
|
||||
answer: 'Refreshed answer',
|
||||
feedbacks: webappMessages.data[0]!.feedbacks?.filter(
|
||||
(feedback) => feedback.from_source === 'user',
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await screen.findByRole('button', { name: 'submit-feedback' })
|
||||
expect(mocks.chatProps).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
config: expect.objectContaining({ supportFeedback: true }),
|
||||
chatList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
adminFeedback: { content: 'Needs work', from_source: 'admin', rating: 'dislike' },
|
||||
feedback: { content: 'Helpful', from_source: 'user', rating: 'like' },
|
||||
feedbackDisabled: false,
|
||||
id: 'message-1',
|
||||
}),
|
||||
]),
|
||||
expect(
|
||||
await screen.findByRole('img', {
|
||||
name: 'appLog.table.header.userRate: appLog.detail.operation.like - Helpful',
|
||||
}),
|
||||
).toBeInTheDocument()
|
||||
await user.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'appLog.table.header.adminRate: appLog.detail.operation.dislike',
|
||||
pressed: true,
|
||||
}),
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'submit-feedback' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks.feedbackMutationFn).toHaveBeenCalledExactlyOnceWith({
|
||||
expect(mocks.feedbackMutationFn).toHaveBeenLastCalledWith({
|
||||
body: {
|
||||
content: undefined,
|
||||
message_id: 'message-1',
|
||||
rating: 'like',
|
||||
rating: null,
|
||||
},
|
||||
params: { agent_id: 'agent-1' },
|
||||
})
|
||||
})
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({
|
||||
queryKey: consoleQuery.agent.byAgentId.logs.get.key(),
|
||||
})
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({
|
||||
queryKey: consoleQuery.agent.byAgentId.logs.byConversationId.messages.get.key(),
|
||||
})
|
||||
expect(await screen.findByText('Refreshed answer')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: 'appLog.table.header.adminRate: appLog.detail.operation.like',
|
||||
pressed: false,
|
||||
}),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('reports operator feedback failures without refreshing log queries', async () => {
|
||||
const user = userEvent.setup()
|
||||
mocks.feedbackMutationFn.mockRejectedValue(new Error('feedback request failed'))
|
||||
const { invalidateQueries } = renderPanel(webappLog, webappMessages)
|
||||
renderPanel(webappLog, webappMessages)
|
||||
|
||||
await user.click(await screen.findByRole('button', { name: 'submit-feedback' }))
|
||||
await user.click(
|
||||
await screen.findByRole('button', {
|
||||
name: 'appLog.table.header.adminRate: appLog.detail.operation.dislike',
|
||||
pressed: true,
|
||||
}),
|
||||
)
|
||||
|
||||
await waitFor(() => expect(mocks.toastError).toHaveBeenCalledWith('feedback request failed'))
|
||||
expect(invalidateQueries).not.toHaveBeenCalled()
|
||||
expect(
|
||||
screen.getByRole('button', {
|
||||
name: 'appLog.table.header.adminRate: appLog.detail.operation.dislike',
|
||||
pressed: true,
|
||||
}),
|
||||
).toBeInTheDocument()
|
||||
expect(mocks.messagesQueryFn).toHaveBeenCalledOnce()
|
||||
expect(mocks.toastSuccess).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -212,14 +209,11 @@ describe('AgentLogDetailPanel', () => {
|
||||
],
|
||||
})
|
||||
|
||||
await screen.findByRole('button', { name: 'submit-feedback' })
|
||||
expect(mocks.chatProps).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
config: expect.objectContaining({ supportFeedback: false }),
|
||||
chatList: expect.arrayContaining([
|
||||
expect.objectContaining({ feedbackDisabled: true, id: 'message-1' }),
|
||||
]),
|
||||
}),
|
||||
)
|
||||
expect(await screen.findByText('Question')).toBeInTheDocument()
|
||||
expect(screen.getByText('Answer')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /appLog.table.header.(adminRate|userRate)/ }),
|
||||
).not.toBeInTheDocument()
|
||||
expect(mocks.feedbackMutationFn).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user