mirror of
https://github.com/langgenius/dify.git
synced 2026-05-11 23:18:39 +08:00
feat(web): improve a11y and remove data-testid (#35999)
This commit is contained in:
parent
9127209dd5
commit
e134c1e0d5
@ -38,13 +38,13 @@ Run these commands from `web/`. From the repository root, prefix them with `pnpm
|
||||
pnpm test
|
||||
|
||||
# Watch mode
|
||||
pnpm test:watch
|
||||
pnpm test --watch
|
||||
|
||||
# Run specific file
|
||||
pnpm test path/to/file.spec.tsx
|
||||
|
||||
# Generate coverage report
|
||||
pnpm test:coverage
|
||||
pnpm test --coverage
|
||||
|
||||
# Analyze component complexity
|
||||
pnpm analyze-component <path>
|
||||
@ -220,7 +220,10 @@ Every test should clearly separate:
|
||||
### 2. Black-Box Testing
|
||||
|
||||
- Test observable behavior, not implementation details
|
||||
- Use semantic queries (getByRole, getByLabelText)
|
||||
- Use semantic queries (`getByRole` with accessible `name`, `getByLabelText`, `getByPlaceholderText`, `getByText`, and scoped `within(...)`)
|
||||
- Treat `getByTestId` as a last resort. If a control cannot be found by role/name, label, landmark, or dialog scope, fix the component accessibility first instead of adding or relying on `data-testid`.
|
||||
- Remove production `data-testid` attributes when semantic selectors can cover the behavior. Keep them only for non-visual mocked boundaries, editor/browser shims such as Monaco, canvas/chart output, or third-party widgets with no accessible DOM in the test environment.
|
||||
- Do not assert decorative icons by test id. Assert the named control that contains them, or mark decorative icons `aria-hidden`.
|
||||
- Avoid testing internal state directly
|
||||
- **Prefer pattern matching over hardcoded strings** in assertions:
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ Then('the shared app page should be accessible', async function (this: DifyWorld
|
||||
|
||||
When('I run the shared workflow app', async function (this: DifyWorld) {
|
||||
const page = this.getPage()
|
||||
const runButton = page.getByTestId('run-button')
|
||||
const runButton = page.getByRole('button', { name: 'Execute' })
|
||||
|
||||
await expect(runButton).toBeEnabled({ timeout: 15_000 })
|
||||
await runButton.click()
|
||||
|
||||
@ -111,7 +111,7 @@ describe('Base Notion Page Selector Flow', () => {
|
||||
await user.type(screen.getByTestId('notion-search-input'), 'missing-page')
|
||||
expect(screen.getByText('common.dataSource.notion.selector.noSearchResult')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByTestId('notion-search-input-clear'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.clear' }))
|
||||
expect(screen.getByTestId('notion-page-name-root-1')).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByTestId('notion-page-preview-root-1'))
|
||||
@ -134,7 +134,7 @@ describe('Base Notion Page Selector Flow', () => {
|
||||
|
||||
expect(onSelectCredential).toHaveBeenCalledWith('c1')
|
||||
|
||||
await user.click(screen.getByTestId('notion-credential-selector-btn'))
|
||||
await user.click(screen.getByRole('combobox', { name: /Workspace 1/ }))
|
||||
await user.click(screen.getByTestId('notion-credential-item-c2'))
|
||||
|
||||
expect(mockInvalidPreImportNotionPages).toHaveBeenCalledWith({ datasetId: 'dataset-1', credentialId: 'c2' })
|
||||
|
||||
@ -119,7 +119,7 @@ describe('RunOnce – integration flow', () => {
|
||||
fireEvent.change(screen.getByPlaceholderText('Bio'), { target: { value: 'Hello' } })
|
||||
|
||||
// Phase 3 – submit
|
||||
fireEvent.click(screen.getByTestId('run-button'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'share.generation.run' }))
|
||||
expect(onSend).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Phase 4 – simulate "running" state
|
||||
@ -132,7 +132,7 @@ describe('RunOnce – integration flow', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const stopBtn = screen.getByTestId('stop-button')
|
||||
const stopBtn = screen.getByRole('button', { name: 'share.generation.stopRun:{"defaultValue":"Stop Run"}' })
|
||||
expect(stopBtn).toBeInTheDocument()
|
||||
fireEvent.click(stopBtn)
|
||||
expect(onStop).toHaveBeenCalledTimes(1)
|
||||
@ -145,7 +145,7 @@ describe('RunOnce – integration flow', () => {
|
||||
runControl={{ onStop, isStopping: true }}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('stop-button')).toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: 'share.generation.stopRun:{"defaultValue":"Stop Run"}' })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('clear resets all field types and allows re-submit', async () => {
|
||||
@ -174,7 +174,7 @@ describe('RunOnce – integration flow', () => {
|
||||
|
||||
// Re-fill and submit
|
||||
fireEvent.change(screen.getByPlaceholderText('Question'), { target: { value: 'New' } })
|
||||
fireEvent.click(screen.getByTestId('run-button'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'share.generation.run' }))
|
||||
expect(onSend).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
@ -212,7 +212,7 @@ describe('RunOnce – integration flow', () => {
|
||||
fireEvent.change(screen.getByPlaceholderText('Text'), { target: { value: 'hello' } })
|
||||
fireEvent.change(screen.getByTestId('code-editor'), { target: { value: '{"a":1}' } })
|
||||
|
||||
fireEvent.click(screen.getByTestId('run-button'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'share.generation.run' }))
|
||||
expect(onSend).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@ -63,12 +63,12 @@ describe('DatasetsLayout', () => {
|
||||
|
||||
render((
|
||||
<DatasetsLayout>
|
||||
<div data-testid="datasets-content">datasets</div>
|
||||
<div>datasets</div>
|
||||
</DatasetsLayout>
|
||||
))
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('datasets-content')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('datasets')).not.toBeInTheDocument()
|
||||
expect(mockReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -80,11 +80,11 @@ describe('DatasetsLayout', () => {
|
||||
|
||||
render((
|
||||
<DatasetsLayout>
|
||||
<div data-testid="datasets-content">datasets</div>
|
||||
<div>datasets</div>
|
||||
</DatasetsLayout>
|
||||
))
|
||||
|
||||
expect(screen.queryByTestId('datasets-content')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('datasets')).not.toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
expect(mockReplace).toHaveBeenCalledWith('/apps')
|
||||
})
|
||||
@ -98,11 +98,11 @@ describe('DatasetsLayout', () => {
|
||||
|
||||
render((
|
||||
<DatasetsLayout>
|
||||
<div data-testid="datasets-content">datasets</div>
|
||||
<div>datasets</div>
|
||||
</DatasetsLayout>
|
||||
))
|
||||
|
||||
expect(screen.getByTestId('datasets-content')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasets')).toBeInTheDocument()
|
||||
expect(mockReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -48,12 +48,12 @@ describe('RoleRouteGuard', () => {
|
||||
|
||||
render((
|
||||
<RoleRouteGuard>
|
||||
<div data-testid="guarded-content">content</div>
|
||||
<div>content</div>
|
||||
</RoleRouteGuard>
|
||||
))
|
||||
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('guarded-content')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('content')).not.toBeInTheDocument()
|
||||
expect(mockReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -64,11 +64,11 @@ describe('RoleRouteGuard', () => {
|
||||
|
||||
render((
|
||||
<RoleRouteGuard>
|
||||
<div data-testid="guarded-content">content</div>
|
||||
<div>content</div>
|
||||
</RoleRouteGuard>
|
||||
))
|
||||
|
||||
expect(screen.queryByTestId('guarded-content')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('content')).not.toBeInTheDocument()
|
||||
await waitFor(() => {
|
||||
expect(mockReplace).toHaveBeenCalledWith('/datasets')
|
||||
})
|
||||
@ -82,11 +82,11 @@ describe('RoleRouteGuard', () => {
|
||||
|
||||
render((
|
||||
<RoleRouteGuard>
|
||||
<div data-testid="guarded-content">content</div>
|
||||
<div>content</div>
|
||||
</RoleRouteGuard>
|
||||
))
|
||||
|
||||
expect(screen.getByTestId('guarded-content')).toBeInTheDocument()
|
||||
expect(screen.getByText('content')).toBeInTheDocument()
|
||||
expect(mockReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -98,11 +98,11 @@ describe('RoleRouteGuard', () => {
|
||||
|
||||
render((
|
||||
<RoleRouteGuard>
|
||||
<div data-testid="guarded-content">content</div>
|
||||
<div>content</div>
|
||||
</RoleRouteGuard>
|
||||
))
|
||||
|
||||
expect(screen.getByTestId('guarded-content')).toBeInTheDocument()
|
||||
expect(screen.getByText('content')).toBeInTheDocument()
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument()
|
||||
expect(mockReplace).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -243,10 +243,7 @@ describe('Filter', () => {
|
||||
)
|
||||
|
||||
// Act
|
||||
const input = screen.getByPlaceholderText('common.operation.search')
|
||||
const clearButton = input.parentElement?.querySelector('div.cursor-pointer')
|
||||
if (clearButton)
|
||||
fireEvent.click(clearButton)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.clear' }))
|
||||
|
||||
// Assert
|
||||
expect(setQueryParams).toHaveBeenCalledWith({ ...queryParams, keyword: '' })
|
||||
|
||||
@ -55,15 +55,23 @@ const BatchAction: FC<IBatchActionProps> = ({
|
||||
<span className="text-[13px] leading-[16px] font-semibold text-text-accent">{t(`${i18nPrefix}.selected`, { ns: 'appAnnotation' })}</span>
|
||||
</div>
|
||||
<Divider type="vertical" className="mx-0.5 h-3.5 bg-divider-regular" />
|
||||
<div className="flex cursor-pointer items-center gap-x-0.5 px-3 py-2" onClick={showDeleteConfirm}>
|
||||
<RiDeleteBinLine className="h-4 w-4 text-components-button-destructive-ghost-text" />
|
||||
<button type="button" className="px-0.5 text-[13px] leading-[16px] font-medium text-components-button-destructive-ghost-text">
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center gap-x-0.5 border-none bg-transparent px-3 py-2 text-left text-components-button-destructive-ghost-text focus-visible:ring-1 focus-visible:ring-state-destructive-border focus-visible:outline-hidden"
|
||||
onClick={showDeleteConfirm}
|
||||
>
|
||||
<RiDeleteBinLine className="h-4 w-4" aria-hidden="true" />
|
||||
<span className="px-0.5 text-[13px] leading-[16px] font-medium">
|
||||
{t('operation.delete', { ns: 'common' })}
|
||||
</button>
|
||||
</div>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<Divider type="vertical" className="mx-0.5 h-3.5 bg-divider-regular" />
|
||||
<button type="button" className="px-3.5 py-2 text-[13px] leading-[16px] font-medium text-components-button-ghost-text" onClick={onCancel}>
|
||||
<button
|
||||
type="button"
|
||||
className="border-none bg-transparent px-3.5 py-2 text-left text-[13px] leading-[16px] font-medium text-components-button-ghost-text focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -54,7 +54,7 @@ describe('CSVUploader', () => {
|
||||
const clickSpy = vi.spyOn(HTMLInputElement.prototype, 'click')
|
||||
renderComponent()
|
||||
|
||||
fireEvent.click(screen.getByText('appAnnotation.batchModal.browse'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'appAnnotation.batchModal.browse' }))
|
||||
|
||||
expect(clickSpy).toHaveBeenCalledTimes(1)
|
||||
clickSpy.mockRestore()
|
||||
@ -137,7 +137,7 @@ describe('CSVUploader', () => {
|
||||
clickSpy.mockRestore()
|
||||
|
||||
const valueSetter = vi.spyOn(fileInput, 'value', 'set')
|
||||
const removeTrigger = screen.getByTestId('remove-file-button')
|
||||
const removeTrigger = screen.getByRole('button', { name: /operation\.delete$/ })
|
||||
fireEvent.click(removeTrigger)
|
||||
|
||||
expect(updateFile).toHaveBeenCalledWith()
|
||||
|
||||
@ -115,6 +115,14 @@ describe('BatchModal', () => {
|
||||
expect(props.onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onCancel when close button is clicked', () => {
|
||||
const { props } = renderComponent()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /operation\.close$/ }))
|
||||
|
||||
expect(props.onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should submit the csv file, poll status, and notify when import completes', async () => {
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true })
|
||||
const { props } = renderComponent()
|
||||
|
||||
@ -97,7 +97,13 @@ const CSVUploader: FC<Props> = ({
|
||||
<CSVIcon className="shrink-0" />
|
||||
<div className="text-text-tertiary">
|
||||
{t('batchModal.csvUploadTitle', { ns: 'appAnnotation' })}
|
||||
<span className="cursor-pointer text-text-accent" onClick={selectHandle}>{t('batchModal.browse', { ns: 'appAnnotation' })}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="inline cursor-pointer border-none bg-transparent p-0 text-left text-text-accent focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={selectHandle}
|
||||
>
|
||||
{t('batchModal.browse', { ns: 'appAnnotation' })}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{dragging && <div ref={dragRef} className="absolute top-0 left-0 h-full w-full" />}
|
||||
@ -113,9 +119,14 @@ const CSVUploader: FC<Props> = ({
|
||||
<div className="hidden items-center group-hover:flex">
|
||||
<Button variant="secondary" onClick={selectHandle}>{t('stepOne.uploader.change', { ns: 'datasetCreation' })}</Button>
|
||||
<div className="mx-2 h-4 w-px bg-divider-regular" />
|
||||
<div className="cursor-pointer p-2" onClick={removeFile} data-testid="remove-file-button">
|
||||
<RiDeleteBinLine className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer border-none bg-transparent p-2 focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
aria-label={t('operation.delete', { ns: 'common' })}
|
||||
onClick={removeFile}
|
||||
>
|
||||
<RiDeleteBinLine className="h-4 w-4 text-text-tertiary" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -91,9 +91,14 @@ const BatchModal: FC<IBatchModalProps> = ({
|
||||
<DialogContent className="w-full max-w-[520px]! overflow-hidden! rounded-xl! border-none px-8 py-6 text-left align-middle">
|
||||
|
||||
<div className="relative pb-1 system-xl-medium text-text-primary">{t('batchModal.title', { ns: 'appAnnotation' })}</div>
|
||||
<div className="absolute top-4 right-4 cursor-pointer p-2" onClick={onCancel}>
|
||||
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-4 right-4 cursor-pointer border-none bg-transparent p-2 focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
aria-label={t('operation.close', { ns: 'common' })}
|
||||
onClick={onCancel}
|
||||
>
|
||||
<RiCloseLine className="h-4 w-4 text-text-tertiary" aria-hidden="true" />
|
||||
</button>
|
||||
<CSVUploader
|
||||
file={currentCSV}
|
||||
updateFile={handleFile}
|
||||
|
||||
@ -212,16 +212,16 @@ describe('SpecificGroupsOrMembers', () => {
|
||||
expect(screen.getByText(baseMember.name)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const groupItem = screen.getByText(baseGroup.name).closest('div')
|
||||
const groupRemove = groupItem?.querySelector('.h-4.w-4.cursor-pointer') as HTMLElement
|
||||
const groupRemove = screen.getAllByRole('button', { name: /operation\.remove$/ })[0]!
|
||||
|
||||
fireEvent.click(groupRemove)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(baseGroup.name)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
const memberItem = screen.getByText(baseMember.name).closest('div')
|
||||
const memberRemove = memberItem?.querySelector('.h-4.w-4.cursor-pointer') as HTMLElement
|
||||
const memberRemove = screen.getAllByRole('button', { name: /operation\.remove$/ })[0]!
|
||||
|
||||
fireEvent.click(memberRemove)
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@ -86,11 +86,13 @@ describe('SpecificGroupsOrMembers', () => {
|
||||
expect(screen.getByText(baseMember.name)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const groupRemove = screen.getByText(baseGroup.name).closest('div')?.querySelector('.h-4.w-4.cursor-pointer') as HTMLElement
|
||||
const removeButtons = screen.getAllByRole('button', { name: /operation\.remove$/ })
|
||||
const groupRemove = removeButtons[0]!
|
||||
const memberRemove = removeButtons[1]!
|
||||
|
||||
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([])
|
||||
})
|
||||
|
||||
@ -119,14 +119,40 @@ function SelectedGroupsBreadCrumb() {
|
||||
const handleReset = useCallback(() => {
|
||||
setSelectedGroupsForBreadcrumb([])
|
||||
}, [setSelectedGroupsForBreadcrumb])
|
||||
const hasBreadcrumb = selectedGroupsForBreadcrumb.length > 0
|
||||
|
||||
return (
|
||||
<div className="flex h-7 items-center gap-x-0.5 px-2 py-0.5">
|
||||
<span className={cn('system-xs-regular text-text-tertiary', selectedGroupsForBreadcrumb.length > 0 && 'cursor-pointer text-text-accent')} onClick={handleReset}>{t('accessControlDialog.operateGroupAndMember.allMembers', { ns: 'app' })}</span>
|
||||
{hasBreadcrumb
|
||||
? (
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer border-none bg-transparent p-0 text-left system-xs-regular text-text-accent focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={handleReset}
|
||||
>
|
||||
{t('accessControlDialog.operateGroupAndMember.allMembers', { ns: 'app' })}
|
||||
</button>
|
||||
)
|
||||
: (
|
||||
<span className="system-xs-regular text-text-tertiary">{t('accessControlDialog.operateGroupAndMember.allMembers', { ns: 'app' })}</span>
|
||||
)}
|
||||
{selectedGroupsForBreadcrumb.map((group, index) => {
|
||||
const isLastGroup = index === selectedGroupsForBreadcrumb.length - 1
|
||||
|
||||
return (
|
||||
<div key={index} className="flex items-center gap-x-0.5 system-xs-regular text-text-tertiary">
|
||||
<span>/</span>
|
||||
<span className={index === selectedGroupsForBreadcrumb.length - 1 ? '' : 'cursor-pointer text-text-accent'} onClick={() => handleBreadCrumbClick(index)}>{group.name}</span>
|
||||
{isLastGroup
|
||||
? <span>{group.name}</span>
|
||||
: (
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer border-none bg-transparent p-0 text-left system-xs-regular text-text-accent focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={() => handleBreadCrumbClick(index)}
|
||||
>
|
||||
{group.name}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
@ -120,6 +120,8 @@ type BaseItemProps = {
|
||||
onRemove?: () => void
|
||||
}
|
||||
function BaseItem({ icon, onRemove, children }: BaseItemProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="group flex flex-row items-center gap-x-1 rounded-full border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark p-1 pr-1.5 shadow-xs">
|
||||
<div className="h-5 w-5 overflow-hidden rounded-full bg-components-icon-bg-blue-solid">
|
||||
@ -128,9 +130,14 @@ function BaseItem({ icon, onRemove, children }: BaseItemProps) {
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
<div className="flex h-4 w-4 cursor-pointer items-center justify-center" onClick={onRemove}>
|
||||
<RiCloseCircleFill className="h-[14px] w-[14px] text-text-quaternary" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-4 w-4 cursor-pointer items-center justify-center border-none bg-transparent p-0 focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
aria-label={t('operation.remove', { ns: 'common' })}
|
||||
onClick={onRemove}
|
||||
>
|
||||
<RiCloseCircleFill className="h-[14px] w-[14px] text-text-quaternary" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -103,6 +103,22 @@ describe('VersionInfoModal', () => {
|
||||
expect(handleClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should close when the close button is clicked', () => {
|
||||
const handleClose = vi.fn()
|
||||
|
||||
render(
|
||||
<VersionInfoModal
|
||||
isOpen
|
||||
onClose={handleClose}
|
||||
onPublish={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'operation.close' }))
|
||||
|
||||
expect(handleClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should validate release note length and clear previous errors before publishing', () => {
|
||||
const handlePublish = vi.fn()
|
||||
const handleClose = vi.fn()
|
||||
|
||||
@ -79,9 +79,14 @@ const VersionInfoModal: FC<VersionInfoModalProps> = ({
|
||||
<div className="title-2xl-semi-bold text-text-primary first-letter:capitalize">
|
||||
{versionInfo?.marked_name ? t('versionHistory.editVersionInfo', { ns: 'workflow' }) : t('versionHistory.nameThisVersion', { ns: 'workflow' })}
|
||||
</div>
|
||||
<div className="absolute top-5 right-5 flex h-8 w-8 cursor-pointer items-center justify-center p-1.5" onClick={onClose}>
|
||||
<RiCloseLine className="h-[18px] w-[18px] text-text-tertiary" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-5 right-5 flex h-8 w-8 cursor-pointer items-center justify-center border-none bg-transparent p-1.5 focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
aria-label={t('operation.close', { ns: 'common' })}
|
||||
onClick={onClose}
|
||||
>
|
||||
<RiCloseLine className="h-[18px] w-[18px] text-text-tertiary" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-4 px-6 py-3">
|
||||
<div className="flex flex-col gap-y-1">
|
||||
|
||||
@ -233,9 +233,7 @@ describe('ConfigVar', () => {
|
||||
const item = screen.getByTitle('name · Name')
|
||||
const itemContainer = item.closest('div.group')
|
||||
expect(itemContainer).not.toBeNull()
|
||||
const actionButtons = itemContainer!.querySelectorAll('div.h-6.w-6')
|
||||
expect(actionButtons).toHaveLength(2)
|
||||
fireEvent.click(actionButtons[0]!)
|
||||
fireEvent.click(within(itemContainer as HTMLElement).getByRole('button', { name: 'common.operation.edit' }))
|
||||
|
||||
const editDialog = await screen.findByRole('dialog')
|
||||
const saveButton = within(editDialog).getByRole('button', { name: 'common.operation.save' })
|
||||
@ -259,9 +257,7 @@ describe('ConfigVar', () => {
|
||||
const item = screen.getByTitle('first · First')
|
||||
const itemContainer = item.closest('div.group')
|
||||
expect(itemContainer).not.toBeNull()
|
||||
const actionButtons = itemContainer!.querySelectorAll('div.h-6.w-6')
|
||||
expect(actionButtons).toHaveLength(2)
|
||||
fireEvent.click(actionButtons[0]!)
|
||||
fireEvent.click(within(itemContainer as HTMLElement).getByRole('button', { name: 'common.operation.edit' }))
|
||||
|
||||
const inputs = await screen.findAllByPlaceholderText('appDebug.variableConfig.inputPlaceholder')
|
||||
fireEvent.change(inputs[0]!, { target: { value: 'second' } })
|
||||
@ -285,9 +281,7 @@ describe('ConfigVar', () => {
|
||||
const item = screen.getByTitle('first · First')
|
||||
const itemContainer = item.closest('div.group')
|
||||
expect(itemContainer).not.toBeNull()
|
||||
const actionButtons = itemContainer!.querySelectorAll('div.h-6.w-6')
|
||||
expect(actionButtons).toHaveLength(2)
|
||||
fireEvent.click(actionButtons[0]!)
|
||||
fireEvent.click(within(itemContainer as HTMLElement).getByRole('button', { name: 'common.operation.edit' }))
|
||||
|
||||
const inputs = await screen.findAllByPlaceholderText('appDebug.variableConfig.inputPlaceholder')
|
||||
fireEvent.change(inputs[1]!, { target: { value: 'Second' } })
|
||||
@ -318,7 +312,7 @@ describe('ConfigVar', () => {
|
||||
onPromptVariablesChange,
|
||||
})
|
||||
|
||||
const removeBtn = screen.getByTestId('var-item-delete-btn')
|
||||
const removeBtn = screen.getByRole('button', { name: 'common.operation.delete' })
|
||||
fireEvent.click(removeBtn)
|
||||
|
||||
expect(onPromptVariablesChange).toHaveBeenCalledWith([])
|
||||
@ -343,7 +337,7 @@ describe('ConfigVar', () => {
|
||||
},
|
||||
)
|
||||
|
||||
const deleteBtn = screen.getByTestId('var-item-delete-btn')
|
||||
const deleteBtn = screen.getByRole('button', { name: 'common.operation.delete' })
|
||||
fireEvent.click(deleteBtn)
|
||||
// confirmation modal should show up
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' }))
|
||||
@ -411,8 +405,7 @@ describe('ConfigVar', () => {
|
||||
const itemContainer = item.closest('div.group')
|
||||
expect(itemContainer).not.toBeNull()
|
||||
|
||||
const actionButtons = itemContainer!.querySelectorAll('div.h-6.w-6')
|
||||
fireEvent.click(actionButtons[0]!)
|
||||
fireEvent.click(within(itemContainer as HTMLElement).getByRole('button', { name: 'common.operation.edit' }))
|
||||
|
||||
const modalState = setShowExternalDataToolModal.mock.calls.at(-1)?.[0]
|
||||
|
||||
@ -460,8 +453,7 @@ describe('ConfigVar', () => {
|
||||
const itemContainer = item.closest('div.group')
|
||||
expect(itemContainer).not.toBeNull()
|
||||
|
||||
const actionButtons = itemContainer!.querySelectorAll('div.h-6.w-6')
|
||||
fireEvent.click(actionButtons[0]!)
|
||||
fireEvent.click(within(itemContainer as HTMLElement).getByRole('button', { name: 'common.operation.edit' }))
|
||||
|
||||
const modalState = setShowExternalDataToolModal.mock.calls.at(-1)?.[0]
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ describe('VarItem', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('var-item-delete-btn'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.delete' }))
|
||||
|
||||
expect(onRemove).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
@ -44,12 +44,7 @@ describe('ConfigSelect Component', () => {
|
||||
|
||||
it('handles option deletion', () => {
|
||||
render(<ConfigSelect {...defaultProps} />)
|
||||
const optionContainer = screen.getByDisplayValue('Option 1').closest('div')
|
||||
const deleteButton = optionContainer?.querySelector('div[role="button"]')
|
||||
|
||||
if (!deleteButton)
|
||||
return
|
||||
fireEvent.click(deleteButton)
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'common.operation.delete' })[0]!)
|
||||
expect(defaultProps.onChange).toHaveBeenCalledWith(['Option 2'])
|
||||
})
|
||||
|
||||
@ -86,7 +81,7 @@ describe('ConfigSelect Component', () => {
|
||||
it('applies delete hover styles', () => {
|
||||
render(<ConfigSelect {...defaultProps} />)
|
||||
const optionContainer = screen.getByDisplayValue('Option 1').closest('div')
|
||||
const deleteButton = optionContainer?.querySelector('div[role="button"]')
|
||||
const deleteButton = screen.getAllByRole('button', { name: 'common.operation.delete' })[0]
|
||||
|
||||
if (!deleteButton)
|
||||
return
|
||||
|
||||
@ -67,9 +67,10 @@ const ConfigSelect: FC<IConfigSelectProps> = ({
|
||||
onFocus={() => setFocusID(index)}
|
||||
onBlur={() => setFocusID(null)}
|
||||
/>
|
||||
<div
|
||||
role="button"
|
||||
className="absolute top-1/2 right-1.5 block translate-y-[-50%] cursor-pointer rounded-md p-1 text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive"
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.delete', { ns: 'common' })}
|
||||
className="absolute top-1/2 right-1.5 block translate-y-[-50%] cursor-pointer rounded-md border-none bg-transparent p-1 text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive focus-visible:ring-1 focus-visible:ring-state-destructive-border focus-visible:outline-hidden"
|
||||
onClick={() => {
|
||||
onChange(options.filter((_, i) => index !== i))
|
||||
setDeletingID(null)
|
||||
@ -77,8 +78,8 @@ const ConfigSelect: FC<IConfigSelectProps> = ({
|
||||
onMouseEnter={() => setDeletingID(index)}
|
||||
onMouseLeave={() => setDeletingID(null)}
|
||||
>
|
||||
<RiDeleteBinLine className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
<RiDeleteBinLine className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</ReactSortable>
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
} from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import { BracketsX as VarIcon } from '@/app/components/base/icons/src/vender/line/development'
|
||||
import IconTypeIcon from './input-type-icon'
|
||||
@ -36,6 +37,7 @@ const VarItem: FC<ItemProps> = ({
|
||||
onRemove,
|
||||
canDrag,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
|
||||
return (
|
||||
@ -58,21 +60,24 @@ const VarItem: FC<ItemProps> = ({
|
||||
<IconTypeIcon type={type as IInputTypeIconProps['type']} className="text-text-tertiary" />
|
||||
</div>
|
||||
<div className={cn('hidden items-center justify-end rounded-lg', !readonly && 'group-hover:flex')}>
|
||||
<div
|
||||
className="mr-1 flex h-6 w-6 cursor-pointer items-center justify-center rounded-md hover:bg-black/5"
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.edit', { ns: 'common' })}
|
||||
className="mr-1 flex h-6 w-6 cursor-pointer items-center justify-center rounded-md border-none bg-transparent p-0 hover:bg-black/5 focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={onEdit}
|
||||
>
|
||||
<RiEditLine className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
<div
|
||||
data-testid="var-item-delete-btn"
|
||||
className="flex h-6 w-6 cursor-pointer items-center justify-center text-text-tertiary hover:text-text-destructive"
|
||||
<RiEditLine className="h-4 w-4 text-text-tertiary" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.delete', { ns: 'common' })}
|
||||
className="flex h-6 w-6 cursor-pointer items-center justify-center border-none bg-transparent p-0 text-text-tertiary hover:text-text-destructive focus-visible:ring-1 focus-visible:ring-state-destructive-border focus-visible:outline-hidden"
|
||||
onClick={onRemove}
|
||||
onMouseOver={() => setIsDeleting(true)}
|
||||
onMouseLeave={() => setIsDeleting(false)}
|
||||
>
|
||||
<RiDeleteBinLine className="h-4 w-4" />
|
||||
</div>
|
||||
<RiDeleteBinLine className="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -368,7 +368,7 @@ describe('AgentTools', () => {
|
||||
|
||||
it('should remove tool when delete action is clicked', async () => {
|
||||
const { getModelConfig } = renderAgentTools()
|
||||
const deleteButton = screen.getByTestId('delete-removed-tool')
|
||||
const deleteButton = screen.getByRole('button', { name: /operation\.delete/i })
|
||||
if (!deleteButton)
|
||||
throw new Error('Delete button not found')
|
||||
await userEvent.click(deleteButton)
|
||||
|
||||
@ -96,6 +96,7 @@ const AgentTools: FC = () => {
|
||||
}
|
||||
|
||||
const [isDeleting, setIsDeleting] = useState<number>(-1)
|
||||
const getDeleteToolLabel = (tool: AgentTool) => `${t('operation.delete', { ns: 'common' })} ${tool.tool_label || tool.tool_name}`
|
||||
const getToolValue = (tool: ToolDefaultValue) => {
|
||||
const currToolInCollections = collectionList.find(c => c.id === tool.provider_id)
|
||||
const currToolWithConfigs = currToolInCollections?.tools.find(t => t.name === tool.tool_name)
|
||||
@ -249,7 +250,7 @@ const AgentTools: FC = () => {
|
||||
<div className="mb-1.5 text-text-tertiary">{t('toolNameUsageTip', { ns: 'tools' })}</div>
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer rounded-sm text-text-accent outline-hidden hover:underline focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
|
||||
className="cursor-pointer rounded-sm border-none bg-transparent p-0 text-left text-text-accent outline-hidden hover:underline focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
|
||||
onClick={() => copy(item.tool_name)}
|
||||
>
|
||||
{t('copyToolName', { ns: 'tools' })}
|
||||
@ -280,8 +281,10 @@ const AgentTools: FC = () => {
|
||||
{t('toolRemoved', { ns: 'tools' })}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div
|
||||
className="cursor-pointer rounded-md p-1 text-text-tertiary hover:text-text-destructive"
|
||||
<button
|
||||
type="button"
|
||||
aria-label={getDeleteToolLabel(item)}
|
||||
className="cursor-pointer rounded-md border-none bg-transparent p-1 text-text-tertiary outline-hidden hover:text-text-destructive focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
|
||||
onClick={() => {
|
||||
const newModelConfig = produce(modelConfig, (draft) => {
|
||||
draft.agentConfig.tools.splice(index, 1)
|
||||
@ -292,8 +295,8 @@ const AgentTools: FC = () => {
|
||||
onMouseOver={() => setIsDeleting(index)}
|
||||
onMouseLeave={() => setIsDeleting(-1)}
|
||||
>
|
||||
<RiDeleteBinLine className="h-4 w-4" />
|
||||
</div>
|
||||
<RiDeleteBinLine className="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!item.isDeleted && !readonly && (
|
||||
@ -320,8 +323,10 @@ const AgentTools: FC = () => {
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<div
|
||||
className="cursor-pointer rounded-md p-1 text-text-tertiary hover:text-text-destructive"
|
||||
<button
|
||||
type="button"
|
||||
aria-label={getDeleteToolLabel(item)}
|
||||
className="cursor-pointer rounded-md border-none bg-transparent p-1 text-text-tertiary outline-hidden hover:text-text-destructive focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
|
||||
onClick={() => {
|
||||
const newModelConfig = produce(modelConfig, (draft) => {
|
||||
draft.agentConfig.tools.splice(index, 1)
|
||||
@ -331,10 +336,9 @@ const AgentTools: FC = () => {
|
||||
}}
|
||||
onMouseOver={() => setIsDeleting(index)}
|
||||
onMouseLeave={() => setIsDeleting(-1)}
|
||||
data-testid="delete-removed-tool"
|
||||
>
|
||||
<RiDeleteBinLine className="h-4 w-4" />
|
||||
</div>
|
||||
<RiDeleteBinLine className="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className={cn(item.isDeleted && 'opacity-50')}>
|
||||
|
||||
@ -82,7 +82,7 @@ describe('InstructionEditor', () => {
|
||||
expect(screen.getByTestId('error-block')).toHaveTextContent('true')
|
||||
expect(screen.getByTestId('last-run-block')).toHaveTextContent('true')
|
||||
|
||||
fireEvent.click(screen.getByText('generate.insertContext'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'generate.insertContext' }))
|
||||
|
||||
expect(mockEmit).toHaveBeenCalledWith(expect.objectContaining({
|
||||
instanceId: 'editor-1',
|
||||
|
||||
@ -113,7 +113,13 @@ const InstructionEditor: FC<Props> = ({
|
||||
<span>{t('generate.press', { ns: 'appDebug' })}</span>
|
||||
<span className="flex h-4 w-3.5 items-center justify-center rounded-sm bg-components-kbd-bg-gray system-kbd text-text-placeholder">/</span>
|
||||
<span>{t('generate.to', { ns: 'appDebug' })}</span>
|
||||
<span onClick={handleInsertVariable} className="ml-1! cursor-pointer hover:border-b hover:border-dotted hover:border-text-tertiary hover:text-text-tertiary">{t('generate.insertContext', { ns: 'appDebug' })}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1! cursor-pointer border-none bg-transparent p-0 text-left hover:border-b hover:border-dotted hover:border-text-tertiary hover:text-text-tertiary focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={handleInsertVariable}
|
||||
>
|
||||
{t('generate.insertContext', { ns: 'appDebug' })}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -218,7 +218,6 @@ const ConfigurationView: FC<ConfigurationViewModel> = ({
|
||||
<DrawerCloseButton
|
||||
aria-label={t('operation.close', { ns: 'common' })}
|
||||
className="h-6 w-6 rounded-md"
|
||||
data-testid="close-icon"
|
||||
/>
|
||||
</div>
|
||||
<Debug
|
||||
|
||||
@ -17,8 +17,8 @@ describe('ContrlBtnGroup', () => {
|
||||
render(<ContrlBtnGroup onSave={onSave} onReset={onReset} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('apply-btn')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('reset-btn')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'appDebug.operation.applyConfig' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'appDebug.operation.resetConfig' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -31,8 +31,8 @@ describe('ContrlBtnGroup', () => {
|
||||
render(<ContrlBtnGroup onSave={onSave} onReset={onReset} />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('apply-btn'))
|
||||
fireEvent.click(screen.getByTestId('reset-btn'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'appDebug.operation.applyConfig' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'appDebug.operation.resetConfig' }))
|
||||
|
||||
// Assert
|
||||
expect(onSave).toHaveBeenCalledTimes(1)
|
||||
|
||||
@ -15,8 +15,8 @@ const ContrlBtnGroup: FC<IContrlBtnGroupProps> = ({ onSave, onReset }) => {
|
||||
return (
|
||||
<div className="fixed bottom-0 left-[224px] h-[64px] w-[519px]">
|
||||
<div className={`${s.ctrlBtn} flex h-full items-center gap-2 bg-white pl-4`}>
|
||||
<Button variant="primary" onClick={onSave} data-testid="apply-btn">{t('operation.applyConfig', { ns: 'appDebug' })}</Button>
|
||||
<Button onClick={onReset} data-testid="reset-btn">{t('operation.resetConfig', { ns: 'appDebug' })}</Button>
|
||||
<Button variant="primary" onClick={onSave}>{t('operation.applyConfig', { ns: 'appDebug' })}</Button>
|
||||
<Button onClick={onReset}>{t('operation.resetConfig', { ns: 'appDebug' })}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -372,7 +372,7 @@ describe('SettingsModal', () => {
|
||||
|
||||
// Act
|
||||
await renderSettingsModal(createDataset())
|
||||
await user.click(screen.getByText('datasetSettings.form.embeddingModelTipLink'))
|
||||
await user.click(screen.getByRole('button', { name: 'datasetSettings.form.embeddingModelTipLink' }))
|
||||
|
||||
// Assert
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
|
||||
|
||||
@ -281,7 +281,13 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
</div>
|
||||
<div className="mt-2 w-full text-xs leading-6 text-text-tertiary">
|
||||
{t('form.embeddingModelTip', { ns: 'datasetSettings' })}
|
||||
<span className="cursor-pointer text-text-accent" onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER })}>{t('form.embeddingModelTipLink', { ns: 'datasetSettings' })}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer border-none bg-transparent p-0 text-left text-text-accent focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER })}
|
||||
>
|
||||
{t('form.embeddingModelTipLink', { ns: 'datasetSettings' })}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -439,7 +439,7 @@ describe('PromptValuePanel', () => {
|
||||
it('collapses the user input panel and hides the clear and run actions', () => {
|
||||
renderPanel()
|
||||
|
||||
fireEvent.click(screen.getByText('appDebug.inputs.userInputField'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'appDebug.inputs.userInputField' }))
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'common.operation.clear' })).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'appDebug.inputs.run' })).not.toBeInTheDocument()
|
||||
|
||||
@ -111,11 +111,15 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
|
||||
<>
|
||||
<div className="relative z-1 mx-3 rounded-xl border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg shadow-md">
|
||||
<div className={cn('px-4 pt-3', userInputFieldCollapse ? 'pb-3' : 'pb-1')}>
|
||||
<div className="flex cursor-pointer items-center gap-0.5 py-0.5" onClick={() => setUserInputFieldCollapse(!userInputFieldCollapse)}>
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center gap-0.5 border-none bg-transparent px-0 py-0.5 text-left focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={() => setUserInputFieldCollapse(!userInputFieldCollapse)}
|
||||
>
|
||||
<div className="system-md-semibold-uppercase text-text-secondary">{t('inputs.userInputField', { ns: 'appDebug' })}</div>
|
||||
{userInputFieldCollapse && <RiArrowRightSLine className="h-4 w-4 text-text-secondary" />}
|
||||
{!userInputFieldCollapse && <RiArrowDownSLine className="h-4 w-4 text-text-secondary" />}
|
||||
</div>
|
||||
{userInputFieldCollapse && <RiArrowRightSLine className="h-4 w-4 text-text-secondary" aria-hidden="true" />}
|
||||
{!userInputFieldCollapse && <RiArrowDownSLine className="h-4 w-4 text-text-secondary" aria-hidden="true" />}
|
||||
</button>
|
||||
{!userInputFieldCollapse && (
|
||||
<div className="mt-1 system-xs-regular text-text-tertiary">{t('inputs.completionVarTip', { ns: 'appDebug' })}</div>
|
||||
)}
|
||||
|
||||
@ -11,12 +11,12 @@ vi.mock('../app-list', () => ({
|
||||
onSuccess: () => void
|
||||
}) {
|
||||
return (
|
||||
<div data-testid="app-list">
|
||||
<button data-testid="app-list-success" onClick={onSuccess}>
|
||||
<div role="region" aria-label="App list">
|
||||
<button type="button" onClick={onSuccess}>
|
||||
Success
|
||||
</button>
|
||||
{onCreateFromBlank && (
|
||||
<button data-testid="create-from-blank" onClick={onCreateFromBlank}>
|
||||
<button type="button" onClick={onCreateFromBlank}>
|
||||
Create from Blank
|
||||
</button>
|
||||
)}
|
||||
@ -48,13 +48,13 @@ describe('CreateAppTemplateDialog', () => {
|
||||
render(<CreateAppTemplateDialog {...defaultProps} show={true} />)
|
||||
|
||||
expect(screen.getByRole('dialog'))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('app-list'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('region', { name: 'App list' }))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render create from blank button when onCreateFromBlank is provided', () => {
|
||||
render(<CreateAppTemplateDialog {...defaultProps} show={true} />)
|
||||
|
||||
expect(screen.getByTestId('create-from-blank'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Create from Blank' }))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render create from blank button when onCreateFromBlank is not provided', () => {
|
||||
@ -62,7 +62,7 @@ describe('CreateAppTemplateDialog', () => {
|
||||
|
||||
render(<CreateAppTemplateDialog {...propsWithoutOnCreate} show={true} />)
|
||||
|
||||
expect(screen.queryByTestId('create-from-blank')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'Create from Blank' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -99,8 +99,8 @@ describe('CreateAppTemplateDialog', () => {
|
||||
const dialog = screen.getByRole('dialog')
|
||||
expect(dialog)!.toBeInTheDocument()
|
||||
|
||||
expect(screen.getByTestId('app-list'))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('app-list-success'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('region', { name: 'App list' }))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'Success' }))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call both onSuccess and onClose when app list success is triggered', () => {
|
||||
@ -115,7 +115,7 @@ describe('CreateAppTemplateDialog', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('app-list-success'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Success' }))
|
||||
|
||||
expect(mockOnSuccess).toHaveBeenCalledTimes(1)
|
||||
expect(mockOnClose).toHaveBeenCalledTimes(1)
|
||||
@ -131,7 +131,7 @@ describe('CreateAppTemplateDialog', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByTestId('create-from-blank'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Create from Blank' }))
|
||||
|
||||
expect(mockOnCreateFromBlank).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@ -186,8 +186,8 @@ describe('CreateAppTemplateDialog', () => {
|
||||
render(<CreateAppTemplateDialog {...propsWithoutOnCreate} show={true} />)
|
||||
}).not.toThrow()
|
||||
|
||||
expect(screen.getByTestId('app-list'))!.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('create-from-blank')).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('region', { name: 'App list' }))!.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'Create from Blank' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should work with all required props only', () => {
|
||||
@ -202,7 +202,7 @@ describe('CreateAppTemplateDialog', () => {
|
||||
}).not.toThrow()
|
||||
|
||||
expect(screen.getByRole('dialog'))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('app-list'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('region', { name: 'App list' }))!.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -27,10 +27,14 @@ export default function Sidebar({ current, categories, onClick, onCreateFromBlan
|
||||
{categories.map(category => (<CategoryItem key={category} category={category} active={current === category} onClick={onClick} />))}
|
||||
</ul>
|
||||
<Divider bgStyle="gradient" />
|
||||
<div className="flex cursor-pointer items-center gap-1 px-3 py-1 text-text-tertiary" onClick={onCreateFromBlank}>
|
||||
<RiStickyNoteAddLine className="h-3.5 w-3.5" />
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full cursor-pointer items-center gap-1 border-none bg-transparent px-3 py-1 text-left text-text-tertiary focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={onCreateFromBlank}
|
||||
>
|
||||
<RiStickyNoteAddLine className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
<span className="system-xs-regular">{t('newApp.startFromBlank', { ns: 'app' })}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -42,19 +46,22 @@ type CategoryItemProps = {
|
||||
}
|
||||
function CategoryItem({ category, active, onClick }: CategoryItemProps) {
|
||||
return (
|
||||
<li
|
||||
className={cn('group flex h-8 cursor-pointer items-center gap-2 rounded-lg p-1 pl-3 hover:bg-state-base-hover [&.active]:bg-state-base-active', active && 'active')}
|
||||
onClick={() => { onClick?.(category) }}
|
||||
>
|
||||
{category === AppCategories.RECOMMENDED && (
|
||||
<div className="inline-flex h-5 w-5 items-center justify-center rounded-md">
|
||||
<RiThumbUpLine className="h-4 w-4 text-components-menu-item-text group-[.active]:text-components-menu-item-text-active" />
|
||||
</div>
|
||||
)}
|
||||
<AppCategoryLabel
|
||||
category={category}
|
||||
className={cn('system-sm-medium text-components-menu-item-text group-hover:text-components-menu-item-text-hover group-[.active]:text-components-menu-item-text-active', active && 'system-sm-semibold')}
|
||||
/>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
className={cn('group flex h-8 w-full cursor-pointer items-center gap-2 rounded-lg border-none bg-transparent p-1 pl-3 text-left hover:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden [&.active]:bg-state-base-active', active && 'active')}
|
||||
onClick={() => { onClick?.(category) }}
|
||||
>
|
||||
{category === AppCategories.RECOMMENDED && (
|
||||
<div className="inline-flex h-5 w-5 items-center justify-center rounded-md">
|
||||
<RiThumbUpLine className="h-4 w-4 text-components-menu-item-text group-[.active]:text-components-menu-item-text-active" aria-hidden="true" />
|
||||
</div>
|
||||
)}
|
||||
<AppCategoryLabel
|
||||
category={category}
|
||||
className={cn('system-sm-medium text-components-menu-item-text group-hover:text-components-menu-item-text-hover group-[.active]:text-components-menu-item-text-active', active && 'system-sm-semibold')}
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
@ -146,11 +146,11 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
||||
<div className="mb-2 flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center border-0 bg-transparent p-0"
|
||||
className="flex cursor-pointer items-center border-0 bg-transparent p-0 text-left focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={() => setIsAppTypeExpanded(!isAppTypeExpanded)}
|
||||
>
|
||||
<span className="system-2xs-medium-uppercase text-text-tertiary">{t('newApp.forBeginners', { ns: 'app' })}</span>
|
||||
<RiArrowRightSLine className={`ml-1 h-4 w-4 text-text-tertiary transition-transform ${isAppTypeExpanded ? 'rotate-90' : ''}`} />
|
||||
<RiArrowRightSLine className={`ml-1 h-4 w-4 text-text-tertiary transition-transform ${isAppTypeExpanded ? 'rotate-90' : ''}`} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
{isAppTypeExpanded && (
|
||||
@ -249,12 +249,16 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
||||
</div>
|
||||
{isAppsFull && <AppsFull className="mt-4" loc="app-create" />}
|
||||
<div className="flex items-center justify-between pt-5 pb-10">
|
||||
<div className="flex cursor-pointer items-center gap-1 system-xs-regular text-text-tertiary" onClick={onCreateFromTemplate}>
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center gap-1 border-none bg-transparent p-0 text-left system-xs-regular text-text-tertiary focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={onCreateFromTemplate}
|
||||
>
|
||||
<span>{t('newApp.noIdeaTip', { ns: 'app' })}</span>
|
||||
<div className="p-px">
|
||||
<RiArrowRightLine className="h-3.5 w-3.5" />
|
||||
<RiArrowRightLine className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={onClose}>{t('newApp.Cancel', { ns: 'app' })}</Button>
|
||||
<Button disabled={isAppsFull || !name} className="gap-1" variant="primary" onClick={handleCreateApp}>
|
||||
|
||||
@ -157,7 +157,7 @@ describe('Uploader', () => {
|
||||
const hiddenInput = getHiddenInput()
|
||||
const clickSpy = vi.spyOn(hiddenInput, 'click')
|
||||
|
||||
fireEvent.click(screen.getByText('dslUploader.browse'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'dslUploader.browse' }))
|
||||
|
||||
expect(clickSpy).toHaveBeenCalled()
|
||||
|
||||
|
||||
@ -112,7 +112,13 @@ const Uploader: FC<Props> = ({
|
||||
<RiUploadCloud2Line className="h-6 w-6 text-text-tertiary" />
|
||||
<div className="text-text-tertiary">
|
||||
{t('dslUploader.button', { ns: 'app' })}
|
||||
<span className="cursor-pointer pl-1 text-text-accent" onClick={selectHandle}>{t('dslUploader.browse', { ns: 'app' })}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="inline cursor-pointer border-none bg-transparent p-0 pl-1 text-left text-text-accent focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={selectHandle}
|
||||
>
|
||||
{t('dslUploader.browse', { ns: 'app' })}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{dragging && <div ref={dragRef} className="absolute top-0 left-0 h-full w-full" />}
|
||||
|
||||
@ -106,6 +106,27 @@ describe('DuplicateAppModal', () => {
|
||||
expect(onHide).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onHide when close button is clicked', async () => {
|
||||
const onHide = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<DuplicateAppModal
|
||||
appName="Demo App"
|
||||
icon_type="emoji"
|
||||
icon="🤖"
|
||||
icon_background="#FFEAD5"
|
||||
show
|
||||
onConfirm={vi.fn()}
|
||||
onHide={onHide}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'operation.close' }))
|
||||
|
||||
expect(onHide).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should restore the original image icon when the picker closes without selecting', async () => {
|
||||
const onConfirm = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
|
||||
@ -72,9 +72,14 @@ const DuplicateAppModal = ({
|
||||
<Dialog open={show}>
|
||||
<DialogContent className="w-full max-w-[480px]! overflow-hidden! border-none px-8 text-left align-middle">
|
||||
|
||||
<div className="absolute top-4 right-4 cursor-pointer p-2" onClick={onHide}>
|
||||
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-4 right-4 cursor-pointer border-none bg-transparent p-2 focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
aria-label={t('operation.close', { ns: 'common' })}
|
||||
onClick={onHide}
|
||||
>
|
||||
<RiCloseLine className="h-4 w-4 text-text-tertiary" aria-hidden="true" />
|
||||
</button>
|
||||
<div className="relative mt-3 mb-9 text-xl leading-[30px] font-semibold text-text-primary">{t('duplicateTitle', { ns: 'app' })}</div>
|
||||
<div className="mb-9 system-sm-regular text-text-secondary">
|
||||
<div className="mb-2 system-md-medium">{t('appCustomize.subTitle', { ns: 'explore' })}</div>
|
||||
|
||||
@ -15,19 +15,19 @@ vi.mock('@/next/navigation', () => ({
|
||||
|
||||
vi.mock('@/app/components/app/annotation', () => ({
|
||||
default: ({ appDetail }: { appDetail: App }) => (
|
||||
<div data-testid="annotation" data-app-id={appDetail.id} />
|
||||
<section aria-label="Annotation log">{appDetail.id}</section>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/log', () => ({
|
||||
default: ({ appDetail }: { appDetail: App }) => (
|
||||
<div data-testid="log" data-app-id={appDetail.id} />
|
||||
<section aria-label="App log">{appDetail.id}</section>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/app/workflow-log', () => ({
|
||||
default: ({ appDetail }: { appDetail: App }) => (
|
||||
<div data-testid="workflow-log" data-app-id={appDetail.id} />
|
||||
<section aria-label="Workflow log">{appDetail.id}</section>
|
||||
),
|
||||
}))
|
||||
|
||||
@ -113,7 +113,7 @@ describe('LogAnnotation', () => {
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByText('appLog.title')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('workflow-log')).toBeInTheDocument()
|
||||
expect(screen.getByRole('region', { name: 'Workflow log' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -127,8 +127,8 @@ describe('LogAnnotation', () => {
|
||||
render(<LogAnnotation pageType={PageType.log} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('log')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('annotation')).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('region', { name: 'App log' })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('region', { name: 'Annotation log' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render annotation content when page type is annotation', () => {
|
||||
@ -139,8 +139,8 @@ describe('LogAnnotation', () => {
|
||||
render(<LogAnnotation pageType={PageType.annotation} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('annotation')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('log')).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('region', { name: 'Annotation log' })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('region', { name: 'App log' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ vi.mock('react-i18next', () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
Trans: ({ i18nKey, components }: { i18nKey: string, components: Record<string, React.ReactNode> }) => (
|
||||
<span data-testid="trans-component" data-i18n-key={i18nKey}>
|
||||
<span>
|
||||
{i18nKey}
|
||||
{components.shareLink}
|
||||
{components.testLink}
|
||||
@ -54,8 +54,7 @@ describe('EmptyElement', () => {
|
||||
const appDetail = createMockAppDetail(AppModeEnum.CHAT)
|
||||
render(<EmptyElement appDetail={appDetail} />)
|
||||
|
||||
const transComponent = screen.getByTestId('trans-component')
|
||||
expect(transComponent).toHaveAttribute('data-i18n-key', 'table.empty.element.content')
|
||||
expect(screen.getByText('table.empty.element.content', { exact: false })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render ThreeDotsIcon SVG', () => {
|
||||
|
||||
@ -146,7 +146,7 @@ describe('Filter', () => {
|
||||
|
||||
render(<Filter {...propsWithKeyword} />)
|
||||
|
||||
const clearButton = screen.getByTestId('input-clear')
|
||||
const clearButton = screen.getByRole('button', { name: 'operation.clear' })
|
||||
fireEvent.click(clearButton)
|
||||
|
||||
expect(mockSetQueryParams).toHaveBeenCalledWith({
|
||||
|
||||
@ -351,52 +351,46 @@ export const AppCardOperations = ({
|
||||
|
||||
if (key === 'launch' && launchConfigAction) {
|
||||
return (
|
||||
<MaybeTooltip
|
||||
key={key}
|
||||
content={t('overview.appInfo.preUseReminder', { ns: 'appOverview' }) ?? ''}
|
||||
tooltipClassName="mt-[-8px]"
|
||||
show={disabled}
|
||||
>
|
||||
<div key={key} className="mr-1 inline-flex">
|
||||
<MaybeTooltip
|
||||
content={t('overview.appInfo.preUseReminder', { ns: 'appOverview' }) ?? ''}
|
||||
tooltipClassName="mt-[-8px]"
|
||||
show={disabled}
|
||||
>
|
||||
<Button
|
||||
className="min-w-[88px] rounded-r-none border-0 px-0 py-0 shadow-none backdrop-blur-none hover:bg-components-button-secondary-bg"
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<div className="flex h-full min-w-[88px] items-center justify-center rounded-l-md px-2 hover:bg-components-button-secondary-bg-hover">
|
||||
<div className="flex items-center justify-center gap-px">
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
<div className="px-[3px] system-xs-medium">{label}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</MaybeTooltip>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="h-6 w-px shrink-0 bg-divider-regular opacity-100"
|
||||
/>
|
||||
<Button
|
||||
className="mr-1 border-0 px-0 py-0 shadow-none backdrop-blur-none hover:bg-components-button-secondary-bg"
|
||||
aria-label={launchConfigAction.label}
|
||||
className="w-8 rounded-l-none border-0 px-0 py-0 shadow-none backdrop-blur-none hover:bg-components-button-secondary-bg-hover"
|
||||
size="small"
|
||||
variant="secondary"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
onClick={launchConfigAction.onClick}
|
||||
disabled={disabled || launchConfigAction.disabled}
|
||||
>
|
||||
<div className="flex h-full min-w-[88px] items-center justify-center rounded-l-md px-2 hover:bg-components-button-secondary-bg-hover">
|
||||
<div className="flex h-full w-8 shrink-0 items-center justify-center rounded-r-md">
|
||||
<div className="flex items-center justify-center gap-px">
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
<div className="px-[3px] system-xs-medium">{label}</div>
|
||||
<RiSettings2Line className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="h-4 w-px shrink-0 bg-divider-regular opacity-100"
|
||||
/>
|
||||
<div
|
||||
className="flex h-full w-8 shrink-0 items-center justify-center rounded-r-md hover:bg-components-button-secondary-bg-hover"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
launchConfigAction.onClick()
|
||||
}}
|
||||
aria-label={launchConfigAction.label}
|
||||
role="button"
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
onKeyDown={(event) => {
|
||||
if (disabled)
|
||||
return
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
launchConfigAction.onClick()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<RiSettings2Line className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
</Button>
|
||||
</MaybeTooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -310,7 +310,7 @@ describe('CustomizeModal', () => {
|
||||
expect(screen.getByText('appOverview.overview.appInfo.customize.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const closeButton = screen.getByTestId('modal-close-button')
|
||||
const closeButton = screen.getByRole('button', { name: 'Close' })
|
||||
fireEvent.click(closeButton)
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
@ -53,7 +53,7 @@ const CustomizeModal: FC<IShareLinkProps> = ({
|
||||
<DialogDescription className="mt-2 body-md-regular text-text-secondary">
|
||||
{t(`${prefixCustomize}.explanation`, { ns: 'appOverview' })}
|
||||
</DialogDescription>
|
||||
<DialogCloseButton data-testid="modal-close-button" />
|
||||
<DialogCloseButton />
|
||||
<div className="mt-4 w-full rounded-lg border-[0.5px] border-components-panel-border px-6 py-5">
|
||||
<Tag bordered={true} hideBg={true} className="border-text-accent-secondary text-text-accent-secondary uppercase">
|
||||
{t(`${prefixCustomize}.way`, { ns: 'appOverview' })}
|
||||
|
||||
@ -273,6 +273,15 @@ describe('SwitchAppModal', () => {
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onClose when close button is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { onClose } = renderComponent()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: /operation\.close$/ }))
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should switch app and navigate with push when keeping original', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
|
||||
@ -111,9 +111,14 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo
|
||||
<Dialog open={show}>
|
||||
<DialogContent className={cn('w-full overflow-hidden! border-none text-left align-middle', cn('w-[600px] max-w-[600px] p-8'))}>
|
||||
|
||||
<div className="absolute top-4 right-4 cursor-pointer p-2" onClick={onClose}>
|
||||
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute top-4 right-4 cursor-pointer border-none bg-transparent p-2 focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
aria-label={t('operation.close', { ns: 'common' })}
|
||||
onClick={onClose}
|
||||
>
|
||||
<RiCloseLine className="h-4 w-4 text-text-tertiary" aria-hidden="true" />
|
||||
</button>
|
||||
<div className="h-12 w-12 rounded-xl border-[0.5px] border-divider-regular bg-background-default-burn p-3 shadow-xl">
|
||||
<AlertTriangle className="h-6 w-6 text-[rgb(247,144,9)]" />
|
||||
</div>
|
||||
@ -161,7 +166,13 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo
|
||||
<div className="flex items-center justify-between pt-6">
|
||||
<div className="flex items-center">
|
||||
<Checkbox className="shrink-0" checked={removeOriginal} onCheck={() => setRemoveOriginal(!removeOriginal)} />
|
||||
<div className="ml-2 cursor-pointer text-sm leading-5 text-text-secondary" onClick={() => setRemoveOriginal(!removeOriginal)}>{t('removeOriginal', { ns: 'app' })}</div>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-2 cursor-pointer border-none bg-transparent p-0 text-left text-sm leading-5 text-text-secondary focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={() => setRemoveOriginal(!removeOriginal)}
|
||||
>
|
||||
{t('removeOriginal', { ns: 'app' })}
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Button className="mr-2" onClick={onClose}>{t('newApp.Cancel', { ns: 'app' })}</Button>
|
||||
|
||||
@ -117,11 +117,9 @@ describe('DetailPanel', () => {
|
||||
})
|
||||
|
||||
it('should render close button', () => {
|
||||
const { container } = render(<DetailPanel runID="run-123" onClose={defaultOnClose} />)
|
||||
render(<DetailPanel runID="run-123" onClose={defaultOnClose} />)
|
||||
|
||||
// Close button has RiCloseLine icon
|
||||
const closeButton = container.querySelector('span.cursor-pointer')
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'common.operation.close' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Run component with correct URLs', () => {
|
||||
@ -173,12 +171,11 @@ describe('DetailPanel', () => {
|
||||
const user = userEvent.setup()
|
||||
const onClose = vi.fn()
|
||||
|
||||
const { container } = render(<DetailPanel runID="run-123" onClose={onClose} />)
|
||||
render(<DetailPanel runID="run-123" onClose={onClose} />)
|
||||
|
||||
const closeButton = container.querySelector('span.cursor-pointer')
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
const closeButton = screen.getByRole('button', { name: 'common.operation.close' })
|
||||
|
||||
await user.click(closeButton!)
|
||||
await user.click(closeButton)
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
@ -328,22 +328,14 @@ describe('Filter', () => {
|
||||
const user = userEvent.setup()
|
||||
const setQueryParams = vi.fn()
|
||||
|
||||
const { container } = render(
|
||||
render(
|
||||
<Filter
|
||||
queryParams={createDefaultQueryParams({ keyword: 'test' })}
|
||||
setQueryParams={setQueryParams}
|
||||
/>,
|
||||
)
|
||||
|
||||
// The Input component renders a clear icon div inside the input wrapper
|
||||
// when showClearIcon is true and value exists
|
||||
const inputWrapper = container.querySelector('.w-\\[200px\\]')
|
||||
|
||||
// Find the clear icon div (has cursor-pointer class and contains RiCloseCircleFill)
|
||||
const clearIconDiv = inputWrapper?.querySelector('div.cursor-pointer')
|
||||
|
||||
expect(clearIconDiv)!.toBeInTheDocument()
|
||||
await user.click(clearIconDiv!)
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.clear' }))
|
||||
|
||||
expect(setQueryParams).toHaveBeenCalledWith({
|
||||
status: 'all',
|
||||
|
||||
@ -27,9 +27,14 @@ const DetailPanel: FC<ILogDetail> = ({ runID, onClose, canReplay = false }) => {
|
||||
|
||||
return (
|
||||
<div className="relative flex grow flex-col pt-3">
|
||||
<span className="absolute top-4 right-3 z-20 cursor-pointer p-1" onClick={onClose}>
|
||||
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.close', { ns: 'common' })}
|
||||
className="absolute top-4 right-3 z-20 cursor-pointer border-none bg-transparent p-1 focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={onClose}
|
||||
>
|
||||
<RiCloseLine className="h-4 w-4 text-text-tertiary" aria-hidden="true" />
|
||||
</button>
|
||||
<div className="flex items-center bg-components-panel-bg">
|
||||
<h1 className="shrink-0 px-4 py-1 system-xl-semibold text-text-primary">{t('runDetail.workflowTitle', { ns: 'appLog' })}</h1>
|
||||
{canReplay && (
|
||||
@ -38,11 +43,11 @@ const DetailPanel: FC<ILogDetail> = ({ runID, onClose, canReplay = false }) => {
|
||||
render={(
|
||||
<button
|
||||
type="button"
|
||||
className="mr-1 flex h-6 w-6 items-center justify-center rounded-md hover:bg-state-base-hover"
|
||||
className="mr-1 flex h-6 w-6 items-center justify-center rounded-md border-none bg-transparent p-0 hover:bg-state-base-hover"
|
||||
aria-label={t('runDetail.testWithParams', { ns: 'appLog' })}
|
||||
onClick={handleReplay}
|
||||
>
|
||||
<RiPlayLargeLine className="h-4 w-4 text-text-tertiary" />
|
||||
<RiPlayLargeLine className="h-4 w-4 text-text-tertiary" aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -130,12 +130,17 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
|
||||
<tr>
|
||||
<td className="w-5 rounded-l-lg bg-background-section-burn pr-1 pl-2 whitespace-nowrap"></td>
|
||||
<td className="bg-background-section-burn py-1.5 pl-3 whitespace-nowrap">
|
||||
<div className="flex cursor-pointer items-center hover:text-text-secondary" onClick={handleSort}>
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center border-none bg-transparent p-0 text-left hover:text-text-secondary focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={handleSort}
|
||||
>
|
||||
{t('table.header.startTime', { ns: 'appLog' })}
|
||||
<ArrowDownIcon
|
||||
className={cn('ml-0.5 h-3 w-3 stroke-current stroke-2 transition-all', 'text-text-tertiary', sortOrder === 'asc' ? 'rotate-180' : '')}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</td>
|
||||
<td className="bg-background-section-burn py-1.5 pl-3 whitespace-nowrap">{t('table.header.status', { ns: 'appLog' })}</td>
|
||||
<td className="bg-background-section-burn py-1.5 pl-3 whitespace-nowrap">{t('table.header.runtime', { ns: 'appLog' })}</td>
|
||||
|
||||
@ -25,8 +25,7 @@ describe('Alert', () => {
|
||||
|
||||
it('should render the close icon', () => {
|
||||
render(<Alert {...defaultProps} />)
|
||||
const closeIcon = screen.getByTestId('close-icon')
|
||||
expect(closeIcon).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'common.operation.close' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -66,7 +65,7 @@ describe('Alert', () => {
|
||||
it('should call onHide when close button is clicked', () => {
|
||||
const onHide = vi.fn()
|
||||
render(<Alert {...defaultProps} onHide={onHide} />)
|
||||
const closeButton = screen.getByTestId('close-icon')
|
||||
const closeButton = screen.getByRole('button', { name: 'common.operation.close' })
|
||||
fireEvent.click(closeButton)
|
||||
expect(onHide).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
@ -172,7 +172,7 @@ describe('AgentLogDetail', () => {
|
||||
|
||||
await renderAndWaitForData()
|
||||
|
||||
fireEvent.click(screen.getByText(/runLog.tracing/i))
|
||||
fireEvent.click(screen.getByRole('button', { name: /runLog.tracing/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
const tracingTab = screen.getByText(/runLog.tracing/i)
|
||||
@ -188,13 +188,13 @@ describe('AgentLogDetail', () => {
|
||||
|
||||
await renderAndWaitForData()
|
||||
|
||||
fireEvent.click(screen.getByText(/runLog.tracing/i))
|
||||
fireEvent.click(screen.getByRole('button', { name: /runLog.tracing/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/runLog.tracing/i).getAttribute('data-active')).toBe('true')
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByText(/runLog.detail/i))
|
||||
fireEvent.click(screen.getByRole('button', { name: /runLog.detail/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
const detailTab = screen.getByText(/runLog.detail/i)
|
||||
|
||||
@ -124,7 +124,7 @@ describe('AgentLogModal', () => {
|
||||
|
||||
render(<AgentLogModal {...mockProps} />)
|
||||
|
||||
const closeBtn = screen.getByRole('heading', { name: /appLog.runDetail.workflowTitle/i }).nextElementSibling!
|
||||
const closeBtn = screen.getByRole('button', { name: 'common.operation.close' })
|
||||
fireEvent.click(closeBtn)
|
||||
|
||||
expect(mockProps.onCancel).toHaveBeenCalledTimes(1)
|
||||
|
||||
@ -67,12 +67,22 @@ const AgentLogDetail: FC<AgentLogDetailProps> = ({ activeTab = 'DETAIL', convers
|
||||
<div className="relative flex grow flex-col">
|
||||
{/* tab */}
|
||||
<div className="flex shrink-0 items-center border-b-[0.5px] border-divider-regular px-4">
|
||||
<div className={cn('mr-6 cursor-pointer border-b-2 border-transparent py-3 text-[13px] leading-[18px] font-semibold text-text-tertiary', currentTab === 'DETAIL' && '!border-[rgb(21,94,239)] text-text-secondary')} data-active={currentTab === 'DETAIL'} onClick={() => switchTab('DETAIL')}>
|
||||
<button
|
||||
type="button"
|
||||
className={cn('mr-6 cursor-pointer border-x-0 border-t-0 border-b-2 border-transparent bg-transparent px-0 py-3 text-left text-[13px] leading-[18px] font-semibold text-text-tertiary focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden', currentTab === 'DETAIL' && '!border-[rgb(21,94,239)] text-text-secondary')}
|
||||
data-active={currentTab === 'DETAIL'}
|
||||
onClick={() => switchTab('DETAIL')}
|
||||
>
|
||||
{t('detail', { ns: 'runLog' })}
|
||||
</div>
|
||||
<div className={cn('mr-6 cursor-pointer border-b-2 border-transparent py-3 text-[13px] leading-[18px] font-semibold text-text-tertiary', currentTab === 'TRACING' && '!border-[rgb(21,94,239)] text-text-secondary')} data-active={currentTab === 'TRACING'} onClick={() => switchTab('TRACING')}>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={cn('mr-6 cursor-pointer border-x-0 border-t-0 border-b-2 border-transparent bg-transparent px-0 py-3 text-left text-[13px] leading-[18px] font-semibold text-text-tertiary focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden', currentTab === 'TRACING' && '!border-[rgb(21,94,239)] text-text-secondary')}
|
||||
data-active={currentTab === 'TRACING'}
|
||||
onClick={() => switchTab('TRACING')}
|
||||
>
|
||||
{t('tracing', { ns: 'runLog' })}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/* panel detail */}
|
||||
<div className={cn('h-0 grow overflow-y-auto rounded-b-2xl bg-components-panel-bg', currentTab !== 'DETAIL' && '!bg-background-section')}>
|
||||
|
||||
@ -46,9 +46,14 @@ const AgentLogModal: FC<AgentLogModalProps> = ({
|
||||
ref={ref}
|
||||
>
|
||||
<h1 className="text-md shrink-0 px-4 py-1 font-semibold text-text-primary">{t('runDetail.workflowTitle', { ns: 'appLog' })}</h1>
|
||||
<span className="absolute top-4 right-3 z-20 cursor-pointer p-1" onClick={onCancel}>
|
||||
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.close', { ns: 'common' })}
|
||||
className="absolute top-4 right-3 z-20 cursor-pointer border-none bg-transparent p-1 focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={onCancel}
|
||||
>
|
||||
<RiCloseLine className="h-4 w-4 text-text-tertiary" aria-hidden="true" />
|
||||
</button>
|
||||
<AgentLogDetail
|
||||
conversationID={currentLogItem.conversationId}
|
||||
messageID={currentLogItem.id}
|
||||
|
||||
@ -3,6 +3,7 @@ import { cva } from 'class-variance-authority'
|
||||
import {
|
||||
memo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type Props = {
|
||||
type?: 'info'
|
||||
@ -26,6 +27,8 @@ const Alert: React.FC<Props> = ({
|
||||
onHide,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={cn('pointer-events-none w-full', className)}>
|
||||
<div
|
||||
@ -41,12 +44,14 @@ const Alert: React.FC<Props> = ({
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pointer-events-auto flex h-6 w-6 cursor-pointer items-center justify-center"
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.close', { ns: 'common' })}
|
||||
className="pointer-events-auto flex h-6 w-6 cursor-pointer items-center justify-center rounded-md border-none bg-transparent p-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-components-button-secondary-accent-border"
|
||||
onClick={onHide}
|
||||
>
|
||||
<span className="i-ri-close-line h-4 w-4 text-text-tertiary" data-testid="close-icon" />
|
||||
</div>
|
||||
<span className="i-ri-close-line h-4 w-4 text-text-tertiary" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -195,10 +195,10 @@ describe('AppIconPicker', () => {
|
||||
const { onSelect } = renderPicker()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId(/emoji-container-/i).length).toBeGreaterThan(0)
|
||||
expect(document.querySelector('em-emoji')?.closest('button'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
const firstEmoji = screen.queryAllByTestId(/emoji-container-/i)[0]
|
||||
const firstEmoji = document.querySelector('em-emoji')?.closest('button')
|
||||
if (!firstEmoji)
|
||||
throw new Error('Could not find emoji option')
|
||||
|
||||
|
||||
@ -88,8 +88,9 @@ const AudioBtn = ({
|
||||
<span className="inline-flex">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={tooltipContent}
|
||||
disabled={audioState === 'loading'}
|
||||
className={`box-border flex h-6 w-6 cursor-pointer items-center justify-center ${isAudition ? 'p-0.5' : 'rounded-md bg-white p-0'}`}
|
||||
className={`box-border flex h-6 w-6 cursor-pointer items-center justify-center border-none bg-transparent ${isAudition ? 'p-0.5' : 'rounded-md bg-white p-0'}`}
|
||||
onClick={handleToggle}
|
||||
>
|
||||
{audioState === 'loading'
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { t } from 'i18next'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
|
||||
@ -11,6 +11,7 @@ type AudioPlayerProps = {
|
||||
srcs?: string[] // Support multiple sources
|
||||
}
|
||||
const AudioPlayer: React.FC<AudioPlayerProps> = ({ src, srcs }) => {
|
||||
const { t } = useTranslation()
|
||||
const [isPlaying, setIsPlaying] = useState(false)
|
||||
const [currentTime, setCurrentTime] = useState(0)
|
||||
const [duration, setDuration] = useState(0)
|
||||
@ -22,6 +23,7 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src, srcs }) => {
|
||||
const [hoverTime, setHoverTime] = useState(0)
|
||||
const [isAudioAvailable, setIsAudioAvailable] = useState(true)
|
||||
const { theme } = useTheme()
|
||||
const playPauseLabel = t(isPlaying ? 'operation.pause' : 'operation.play', { ns: 'common' })
|
||||
useEffect(() => {
|
||||
const audio = audioRef.current
|
||||
/* v8 ignore next 2 - @preserve */
|
||||
@ -245,10 +247,10 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src, srcs }) => {
|
||||
{/* If srcs array is provided, render multiple source elements */}
|
||||
{srcs && srcs.map((srcUrl, index) => (<source key={index} src={srcUrl} />))}
|
||||
</audio>
|
||||
<button type="button" data-testid="play-pause-btn" className="inline-flex shrink-0 cursor-pointer items-center justify-center border-none text-text-accent transition-all hover:text-text-accent-secondary disabled:text-components-button-primary-bg-disabled" onClick={togglePlay} disabled={!isAudioAvailable}>
|
||||
<button type="button" aria-label={playPauseLabel} className="inline-flex shrink-0 cursor-pointer items-center justify-center border-none text-text-accent transition-all hover:text-text-accent-secondary disabled:text-components-button-primary-bg-disabled" onClick={togglePlay} disabled={!isAudioAvailable}>
|
||||
{isPlaying
|
||||
? (<div className="i-ri-pause-circle-fill h-5 w-5" />)
|
||||
: (<div className="i-ri-play-large-fill h-5 w-5" />)}
|
||||
? (<div className="i-ri-pause-circle-fill h-5 w-5" aria-hidden="true" />)
|
||||
: (<div className="i-ri-play-large-fill h-5 w-5" aria-hidden="true" />)}
|
||||
</button>
|
||||
<div className={cn(isAudioAvailable && 'grow')} hidden={!isAudioAvailable}>
|
||||
<div className="flex h-8 items-center justify-center">
|
||||
|
||||
@ -50,6 +50,9 @@ function getReactProps<T extends Element>(el: T): Record<string, ReactEventHandl
|
||||
return key ? (el as unknown as Record<string, Record<string, ReactEventHandler>>)[key]! : {}
|
||||
}
|
||||
|
||||
const getPlayButton = () => screen.getByRole('button', { name: 'common.operation.play' })
|
||||
const getPauseButton = () => screen.getByRole('button', { name: 'common.operation.pause' })
|
||||
|
||||
// ─── Setup / teardown ─────────────────────────────────────────────────────────
|
||||
|
||||
beforeEach(() => {
|
||||
@ -77,7 +80,7 @@ describe('AudioPlayer — rendering', () => {
|
||||
it('should render the play button and audio element when given a src', () => {
|
||||
render(<AudioPlayer src="https://example.com/a.mp3" />)
|
||||
|
||||
expect(screen.getByTestId('play-pause-btn'))!.toBeInTheDocument()
|
||||
expect(getPlayButton())!.toBeInTheDocument()
|
||||
expect(document.querySelector('audio'))!.toBeInTheDocument()
|
||||
expect(document.querySelector('audio')?.getAttribute('src')).toBe('https://example.com/a.mp3')
|
||||
})
|
||||
@ -93,7 +96,7 @@ describe('AudioPlayer — rendering', () => {
|
||||
|
||||
it('should render without crashing when no props are supplied', () => {
|
||||
render(<AudioPlayer />)
|
||||
expect(screen.getByTestId('play-pause-btn'))!.toBeInTheDocument()
|
||||
expect(getPlayButton())!.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -102,7 +105,7 @@ describe('AudioPlayer — rendering', () => {
|
||||
describe('AudioPlayer — play/pause', () => {
|
||||
it('should call audio.play() on first button click', async () => {
|
||||
render(<AudioPlayer src="https://example.com/a.mp3" />)
|
||||
const btn = screen.getByTestId('play-pause-btn')
|
||||
const btn = getPlayButton()
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(btn)
|
||||
@ -113,13 +116,13 @@ describe('AudioPlayer — play/pause', () => {
|
||||
|
||||
it('should call audio.pause() on second button click', async () => {
|
||||
render(<AudioPlayer src="https://example.com/a.mp3" />)
|
||||
const btn = screen.getByTestId('play-pause-btn')
|
||||
const btn = getPlayButton()
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(btn)
|
||||
})
|
||||
await act(async () => {
|
||||
fireEvent.click(btn)
|
||||
fireEvent.click(getPauseButton())
|
||||
})
|
||||
|
||||
expect(HTMLMediaElement.prototype.pause).toHaveBeenCalledTimes(1)
|
||||
@ -127,7 +130,7 @@ describe('AudioPlayer — play/pause', () => {
|
||||
|
||||
it('should show the pause icon while playing and play icon while paused', async () => {
|
||||
render(<AudioPlayer src="https://example.com/a.mp3" />)
|
||||
const btn = screen.getByTestId('play-pause-btn')
|
||||
const btn = getPlayButton()
|
||||
|
||||
expect(btn.querySelector('.i-ri-play-large-fill'))!.toBeInTheDocument()
|
||||
expect(btn.querySelector('.i-ri-pause-circle-fill')).not.toBeInTheDocument()
|
||||
@ -136,23 +139,25 @@ describe('AudioPlayer — play/pause', () => {
|
||||
fireEvent.click(btn)
|
||||
})
|
||||
|
||||
expect(btn.querySelector('.i-ri-pause-circle-fill'))!.toBeInTheDocument()
|
||||
expect(btn.querySelector('.i-ri-play-large-fill')).not.toBeInTheDocument()
|
||||
const pauseBtn = getPauseButton()
|
||||
expect(pauseBtn.querySelector('.i-ri-pause-circle-fill'))!.toBeInTheDocument()
|
||||
expect(pauseBtn.querySelector('.i-ri-play-large-fill')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should reset to stopped state when the audio ends', async () => {
|
||||
render(<AudioPlayer src="https://example.com/a.mp3" />)
|
||||
const btn = screen.getByTestId('play-pause-btn')
|
||||
const btn = getPlayButton()
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(btn)
|
||||
})
|
||||
expect(btn.querySelector('.i-ri-pause-circle-fill'))!.toBeInTheDocument()
|
||||
expect(getPauseButton().querySelector('.i-ri-pause-circle-fill'))!.toBeInTheDocument()
|
||||
|
||||
const audio = document.querySelector('audio') as HTMLAudioElement
|
||||
await act(async () => {
|
||||
audio.dispatchEvent(new Event('ended'))
|
||||
})
|
||||
expect(getPlayButton().querySelector('.i-ri-play-large-fill'))!.toBeInTheDocument()
|
||||
|
||||
expect(btn.querySelector('.i-ri-play-large-fill'))!.toBeInTheDocument()
|
||||
})
|
||||
@ -165,7 +170,7 @@ describe('AudioPlayer — play/pause', () => {
|
||||
audio.dispatchEvent(new Event('error'))
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('play-pause-btn'))!.toBeDisabled()
|
||||
expect(getPlayButton())!.toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -216,7 +221,7 @@ describe('AudioPlayer — audio events', () => {
|
||||
audio.dispatchEvent(new Event('error'))
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('play-pause-btn'))!.toBeDisabled()
|
||||
expect(getPlayButton())!.toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -276,7 +281,7 @@ describe('AudioPlayer — waveform generation', () => {
|
||||
render(<AudioPlayer srcs={['blob:something']} />)
|
||||
await advanceWaveformTimer()
|
||||
|
||||
expect(screen.getByTestId('play-pause-btn'))!.toBeDisabled()
|
||||
expect(getPlayButton())!.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should not trigger waveform generation when no src or srcs provided', async () => {
|
||||
@ -462,7 +467,7 @@ describe('AudioPlayer — missing coverage', () => {
|
||||
vi.spyOn(HTMLMediaElement.prototype, 'play').mockRejectedValue(new Error('play failed'))
|
||||
|
||||
render(<AudioPlayer src="https://example.com/audio.mp3" />)
|
||||
const btn = screen.getByTestId('play-pause-btn')
|
||||
const btn = getPlayButton()
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(btn)
|
||||
@ -530,7 +535,7 @@ describe('AudioPlayer — missing coverage', () => {
|
||||
render(<AudioPlayer src="blob:https://example.com" />)
|
||||
await advanceWaveformTimer() // sets isAudioAvailable to false (invalid protocol)
|
||||
|
||||
const btn = screen.getByTestId('play-pause-btn')
|
||||
const btn = getPlayButton()
|
||||
await act(async () => {
|
||||
fireEvent.click(btn)
|
||||
})
|
||||
@ -549,7 +554,7 @@ describe('AudioPlayer — missing coverage', () => {
|
||||
audio.dispatchEvent(new Event('error'))
|
||||
})
|
||||
|
||||
const btn = screen.getByTestId('play-pause-btn')
|
||||
const btn = getPlayButton()
|
||||
const props = getReactProps(btn)
|
||||
|
||||
await act(async () => {
|
||||
@ -606,7 +611,7 @@ describe('AudioPlayer — additional branch coverage', () => {
|
||||
audio.dispatchEvent(new Event('error'))
|
||||
})
|
||||
|
||||
expect(screen.queryByTestId('play-pause-btn'))!.toBeDisabled()
|
||||
expect(getPlayButton())!.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should update current time on timeupdate event', async () => {
|
||||
@ -627,7 +632,7 @@ describe('AudioPlayer — additional branch coverage', () => {
|
||||
audio.dispatchEvent(new Event('error'))
|
||||
})
|
||||
|
||||
const btn = screen.getByTestId('play-pause-btn')
|
||||
const btn = getPlayButton()
|
||||
await act(async () => {
|
||||
fireEvent.click(btn)
|
||||
})
|
||||
|
||||
@ -1333,10 +1333,10 @@ describe('ChatWrapper', () => {
|
||||
|
||||
render(<ChatWrapper />)
|
||||
|
||||
fireEvent.click(await screen.findByTestId('edit-btn'))
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'common.operation.edit' }))
|
||||
const editedTextarea = await screen.findByDisplayValue('Original question')
|
||||
fireEvent.change(editedTextarea, { target: { value: 'Edited question text' } })
|
||||
fireEvent.click(screen.getByTestId('save-edit-btn'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(handleSend).toHaveBeenCalledWith(
|
||||
|
||||
@ -184,7 +184,7 @@ describe('HeaderInMobile', () => {
|
||||
render(<HeaderInMobile />)
|
||||
|
||||
// Open dropdown (More button)
|
||||
fireEvent.click(await screen.findByTestId('mobile-more-btn'))
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'common.operation.more' }))
|
||||
|
||||
// Find and click "View Chat Settings"
|
||||
await waitFor(() => {
|
||||
@ -213,7 +213,7 @@ describe('HeaderInMobile', () => {
|
||||
render(<HeaderInMobile />)
|
||||
|
||||
// Open dropdown and chat settings
|
||||
fireEvent.click(await screen.findByTestId('mobile-more-btn'))
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'common.operation.more' }))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/share\.chat\.viewChatSettings/i))!.toBeInTheDocument()
|
||||
})
|
||||
@ -241,7 +241,7 @@ describe('HeaderInMobile', () => {
|
||||
render(<HeaderInMobile />)
|
||||
|
||||
// Open dropdown
|
||||
fireEvent.click(await screen.findByTestId('mobile-more-btn'))
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'common.operation.more' }))
|
||||
|
||||
// "View Chat Settings" should not be present
|
||||
await waitFor(() => {
|
||||
@ -259,7 +259,7 @@ describe('HeaderInMobile', () => {
|
||||
render(<HeaderInMobile />)
|
||||
|
||||
// Open dropdown
|
||||
fireEvent.click(await screen.findByTestId('mobile-more-btn'))
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'common.operation.more' }))
|
||||
|
||||
// Click "New Conversation" or "Reset Chat"
|
||||
await waitFor(() => {
|
||||
|
||||
@ -19,7 +19,7 @@ describe('MobileOperationDropdown Component', () => {
|
||||
render(<MobileOperationDropdown {...defaultProps} />)
|
||||
|
||||
// Trigger button should be present (ActionButton renders a button)
|
||||
const trigger = screen.getByRole('button')
|
||||
const trigger = screen.getByRole('button', { name: 'common.operation.more' })
|
||||
expect(trigger).toBeInTheDocument()
|
||||
|
||||
// Menu should be hidden initially
|
||||
@ -39,7 +39,7 @@ describe('MobileOperationDropdown Component', () => {
|
||||
const user = userEvent.setup()
|
||||
render(<MobileOperationDropdown {...defaultProps} hideViewChatSettings={true} />)
|
||||
|
||||
await user.click(screen.getByRole('button'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.more' }))
|
||||
|
||||
expect(screen.getByText('share.chat.resetChat')).toBeInTheDocument()
|
||||
expect(screen.queryByText('share.chat.viewChatSettings')).not.toBeInTheDocument()
|
||||
@ -49,7 +49,7 @@ describe('MobileOperationDropdown Component', () => {
|
||||
const user = userEvent.setup()
|
||||
render(<MobileOperationDropdown {...defaultProps} />)
|
||||
|
||||
await user.click(screen.getByRole('button'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.more' }))
|
||||
|
||||
// Reset Chat
|
||||
await user.click(screen.getByText('share.chat.resetChat'))
|
||||
@ -57,7 +57,7 @@ describe('MobileOperationDropdown Component', () => {
|
||||
expect(defaultProps.handleResetChat).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
await user.click(screen.getByRole('button'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.more' }))
|
||||
// View Chat Settings
|
||||
await user.click(screen.getByText('share.chat.viewChatSettings'))
|
||||
await waitFor(() => {
|
||||
@ -68,7 +68,7 @@ describe('MobileOperationDropdown Component', () => {
|
||||
it('applies hover state to ActionButton when open', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<MobileOperationDropdown {...defaultProps} />)
|
||||
const trigger = screen.getByRole('button')
|
||||
const trigger = screen.getByRole('button', { name: 'common.operation.more' })
|
||||
|
||||
// closed state
|
||||
expect(trigger).not.toHaveClass('action-btn-hover')
|
||||
@ -82,7 +82,7 @@ describe('MobileOperationDropdown Component', () => {
|
||||
const user = userEvent.setup()
|
||||
render(<MobileOperationDropdown {...defaultProps} />)
|
||||
|
||||
await user.click(screen.getByRole('button'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.more' }))
|
||||
await user.click(screen.getByText('share.chat.resetChat'))
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@ -32,13 +32,16 @@ const MobileOperationDropdown = ({
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<DropdownMenuTrigger
|
||||
render={<div />}
|
||||
data-testid="mobile-more-btn"
|
||||
>
|
||||
<ActionButton size="l" state={open ? ActionButtonState.Hover : ActionButtonState.Default}>
|
||||
<div className="i-ri-more-fill h-[18px] w-[18px]" />
|
||||
</ActionButton>
|
||||
</DropdownMenuTrigger>
|
||||
render={(
|
||||
<ActionButton
|
||||
aria-label={t('operation.more', { ns: 'common' })}
|
||||
size="l"
|
||||
state={open ? ActionButtonState.Hover : ActionButtonState.Default}
|
||||
>
|
||||
<div className="i-ri-more-fill h-[18px] w-[18px]" aria-hidden="true" />
|
||||
</ActionButton>
|
||||
)}
|
||||
/>
|
||||
<DropdownMenuContent
|
||||
placement="bottom-end"
|
||||
sideOffset={4}
|
||||
|
||||
@ -7,9 +7,9 @@ import Item from '../item'
|
||||
vi.mock('@/app/components/base/chat/chat-with-history/sidebar/operation', () => ({
|
||||
default: ({ togglePin, onRenameConversation, onDelete, isItemHovering, isActive, isPinned }: { togglePin: () => void, onRenameConversation: () => void, onDelete: () => void, isItemHovering: boolean, isActive: boolean, isPinned: boolean }) => (
|
||||
<div data-testid="mock-operation">
|
||||
<button onClick={togglePin} data-testid="pin-button">Pin</button>
|
||||
<button onClick={onRenameConversation} data-testid="rename-button">Rename</button>
|
||||
<button onClick={onDelete} data-testid="delete-button">Delete</button>
|
||||
<button onClick={togglePin}>Pin</button>
|
||||
<button onClick={onRenameConversation}>Rename</button>
|
||||
<button onClick={onDelete}>Delete</button>
|
||||
<span data-hovering={isItemHovering} data-testid="hover-indicator">Hovering</span>
|
||||
<span data-active={isActive} data-testid="active-indicator">Active</span>
|
||||
<span data-pinned={isPinned} data-testid="pinned-indicator">Pinned</span>
|
||||
@ -137,7 +137,7 @@ describe('Item', () => {
|
||||
const onOperate = vi.fn()
|
||||
render(<Item {...defaultProps} onOperate={onOperate} isPin={true} />)
|
||||
|
||||
await user.click(screen.getByTestId('pin-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'Pin' }))
|
||||
expect(onOperate).toHaveBeenCalledWith('unpin', mockItem)
|
||||
})
|
||||
|
||||
@ -146,7 +146,7 @@ describe('Item', () => {
|
||||
const onOperate = vi.fn()
|
||||
render(<Item {...defaultProps} onOperate={onOperate} isPin={false} />)
|
||||
|
||||
await user.click(screen.getByTestId('pin-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'Pin' }))
|
||||
expect(onOperate).toHaveBeenCalledWith('pin', mockItem)
|
||||
})
|
||||
|
||||
@ -155,7 +155,7 @@ describe('Item', () => {
|
||||
const onOperate = vi.fn()
|
||||
render(<Item {...defaultProps} onOperate={onOperate} />)
|
||||
|
||||
await user.click(screen.getByTestId('pin-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'Pin' }))
|
||||
expect(onOperate).toHaveBeenCalledWith('pin', mockItem)
|
||||
})
|
||||
})
|
||||
@ -213,7 +213,7 @@ describe('Item', () => {
|
||||
const onChangeConversation = vi.fn()
|
||||
render(<Item {...defaultProps} onChangeConversation={onChangeConversation} />)
|
||||
|
||||
const deleteButton = screen.getByTestId('delete-button')
|
||||
const deleteButton = screen.getByRole('button', { name: 'Delete' })
|
||||
await user.click(deleteButton)
|
||||
|
||||
// onChangeConversation should not be called when Operation button is clicked
|
||||
@ -225,7 +225,7 @@ describe('Item', () => {
|
||||
const onOperate = vi.fn()
|
||||
render(<Item {...defaultProps} onOperate={onOperate} />)
|
||||
|
||||
await user.click(screen.getByTestId('delete-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'Delete' }))
|
||||
expect(onOperate).toHaveBeenCalledWith('delete', mockItem)
|
||||
})
|
||||
|
||||
@ -234,7 +234,7 @@ describe('Item', () => {
|
||||
const onOperate = vi.fn()
|
||||
render(<Item {...defaultProps} onOperate={onOperate} />)
|
||||
|
||||
await user.click(screen.getByTestId('rename-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'Rename' }))
|
||||
expect(onOperate).toHaveBeenCalledWith('rename', mockItem)
|
||||
})
|
||||
|
||||
@ -243,9 +243,9 @@ describe('Item', () => {
|
||||
const onOperate = vi.fn()
|
||||
render(<Item {...defaultProps} onOperate={onOperate} />)
|
||||
|
||||
await user.click(screen.getByTestId('rename-button'))
|
||||
await user.click(screen.getByTestId('pin-button'))
|
||||
await user.click(screen.getByTestId('delete-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'Rename' }))
|
||||
await user.click(screen.getByRole('button', { name: 'Pin' }))
|
||||
await user.click(screen.getByRole('button', { name: 'Delete' }))
|
||||
|
||||
expect(onOperate).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
@ -296,13 +296,13 @@ describe('Item', () => {
|
||||
const onOperate = vi.fn()
|
||||
render(<Item {...defaultProps} onOperate={onOperate} />)
|
||||
|
||||
await user.click(screen.getByTestId('rename-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'Rename' }))
|
||||
expect(onOperate).toHaveBeenNthCalledWith(1, 'rename', mockItem)
|
||||
|
||||
await user.click(screen.getByTestId('pin-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'Pin' }))
|
||||
expect(onOperate).toHaveBeenNthCalledWith(2, 'pin', mockItem)
|
||||
|
||||
await user.click(screen.getByTestId('delete-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'Delete' }))
|
||||
expect(onOperate).toHaveBeenNthCalledWith(3, 'delete', mockItem)
|
||||
})
|
||||
|
||||
@ -314,12 +314,12 @@ describe('Item', () => {
|
||||
<Item {...defaultProps} onOperate={onOperate} isPin={false} />,
|
||||
)
|
||||
|
||||
await user.click(screen.getByTestId('pin-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'Pin' }))
|
||||
expect(onOperate).toHaveBeenCalledWith('pin', mockItem)
|
||||
|
||||
rerender(<Item {...defaultProps} onOperate={onOperate} isPin={true} />)
|
||||
|
||||
await user.click(screen.getByTestId('pin-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'Pin' }))
|
||||
expect(onOperate).toHaveBeenCalledWith('unpin', mockItem)
|
||||
})
|
||||
})
|
||||
@ -412,7 +412,7 @@ describe('Item', () => {
|
||||
|
||||
rerender(<Item {...defaultProps} onOperate={newOnOperate} />)
|
||||
|
||||
await user.click(screen.getByTestId('delete-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'Delete' }))
|
||||
|
||||
expect(newOnOperate).toHaveBeenCalledWith('delete', mockItem)
|
||||
expect(oldOnOperate).not.toHaveBeenCalled()
|
||||
|
||||
@ -98,7 +98,7 @@ describe('ChatLogModals', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByTestId('close-btn-container'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.close' }))
|
||||
|
||||
expect(setCurrentLogItem).toHaveBeenCalled()
|
||||
expect(setShowPromptLogModal).toHaveBeenCalledWith(false)
|
||||
|
||||
@ -183,7 +183,7 @@ describe('Question component', () => {
|
||||
|
||||
renderWithProvider(makeItem())
|
||||
|
||||
const copyBtn = screen.getByTestId('copy-btn')
|
||||
const copyBtn = screen.getByRole('button', { name: 'common.operation.copy' })
|
||||
await user.click(copyBtn)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -195,8 +195,8 @@ describe('Question component', () => {
|
||||
it('should not show edit action when enableEdit is false', () => {
|
||||
renderWithProvider(makeItem(), vi.fn() as unknown as OnRegenerate, { enableEdit: false })
|
||||
|
||||
expect(screen.getByTestId('copy-btn')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('edit-btn')).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'common.operation.copy' })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'common.operation.edit' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should enter edit mode when edit action clicked, allow editing and call onRegenerate on resend', async () => {
|
||||
@ -206,7 +206,7 @@ describe('Question component', () => {
|
||||
const item = makeItem()
|
||||
renderWithProvider(item, onRegenerate)
|
||||
|
||||
const editBtn = screen.getByTestId('edit-btn')
|
||||
const editBtn = screen.getByRole('button', { name: 'common.operation.edit' })
|
||||
await user.click(editBtn)
|
||||
|
||||
const textbox = await screen.findByRole('textbox')
|
||||
@ -227,14 +227,14 @@ describe('Question component', () => {
|
||||
const user = userEvent.setup()
|
||||
const { container } = renderWithProvider(makeItem())
|
||||
|
||||
const editBtn = screen.getByTestId('edit-btn')
|
||||
const editBtn = screen.getByRole('button', { name: 'common.operation.edit' })
|
||||
await user.click(editBtn)
|
||||
|
||||
const textbox = await screen.findByRole('textbox')
|
||||
await user.clear(textbox)
|
||||
await user.type(textbox, 'Edited question')
|
||||
|
||||
const cancelBtn = await screen.findByTestId('cancel-edit-btn')
|
||||
const cancelBtn = await screen.findByRole('button', { name: 'common.operation.cancel' })
|
||||
await user.click(cancelBtn)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -250,7 +250,7 @@ describe('Question component', () => {
|
||||
|
||||
renderWithProvider(makeItem(), onRegenerate)
|
||||
|
||||
await user.click(screen.getByTestId('edit-btn'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.edit' }))
|
||||
const textbox = await screen.findByRole('textbox')
|
||||
|
||||
await user.clear(textbox)
|
||||
@ -269,7 +269,7 @@ describe('Question component', () => {
|
||||
|
||||
renderWithProvider(makeItem(), onRegenerate)
|
||||
|
||||
await user.click(screen.getByTestId('edit-btn'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.edit' }))
|
||||
const textbox = await screen.findByRole('textbox')
|
||||
|
||||
await user.clear(textbox)
|
||||
@ -285,7 +285,7 @@ describe('Question component', () => {
|
||||
|
||||
renderWithProvider(makeItem(), onRegenerate)
|
||||
|
||||
fireEvent.click(screen.getByTestId('edit-btn'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.edit' }))
|
||||
const textbox = screen.getByRole('textbox')
|
||||
|
||||
fireEvent.compositionStart(textbox)
|
||||
@ -302,7 +302,7 @@ describe('Question component', () => {
|
||||
const onRegenerate = vi.fn() as unknown as OnRegenerate
|
||||
renderWithProvider(makeItem(), onRegenerate)
|
||||
|
||||
fireEvent.click(screen.getByTestId('edit-btn'))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.edit' }))
|
||||
const textbox = screen.getByRole('textbox')
|
||||
fireEvent.change(textbox, { target: { value: 'IME guard text' } })
|
||||
|
||||
@ -392,7 +392,7 @@ describe('Question component', () => {
|
||||
|
||||
renderWithProvider(item, onRegenerate)
|
||||
|
||||
const editBtn = screen.getByTestId('edit-btn')
|
||||
const editBtn = screen.getByRole('button', { name: 'common.operation.edit' })
|
||||
await user.click(editBtn)
|
||||
|
||||
const textbox = await screen.findByRole('textbox')
|
||||
@ -431,7 +431,7 @@ describe('Question component', () => {
|
||||
|
||||
renderWithProvider(item, onRegenerate)
|
||||
|
||||
await user.click(screen.getByTestId('edit-btn'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.edit' }))
|
||||
const textbox = await screen.findByRole('textbox')
|
||||
|
||||
// Press Shift+Enter
|
||||
@ -445,7 +445,7 @@ describe('Question component', () => {
|
||||
const onRegenerate = vi.fn() as unknown as OnRegenerate
|
||||
renderWithProvider(makeItem(), onRegenerate)
|
||||
|
||||
await user.click(screen.getByTestId('edit-btn'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.edit' }))
|
||||
const textbox = await screen.findByRole('textbox')
|
||||
|
||||
// Create an event with nativeEvent.isComposing = true
|
||||
@ -461,7 +461,7 @@ describe('Question component', () => {
|
||||
const onRegenerate = vi.fn() as unknown as OnRegenerate
|
||||
const { unmount } = renderWithProvider(makeItem(), onRegenerate)
|
||||
|
||||
await user.click(screen.getByTestId('edit-btn'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.edit' }))
|
||||
const textbox = await screen.findByRole('textbox')
|
||||
|
||||
fireEvent.compositionStart(textbox)
|
||||
@ -471,11 +471,11 @@ describe('Question component', () => {
|
||||
fireEvent.compositionStart(textbox)
|
||||
fireEvent.compositionEnd(textbox)
|
||||
|
||||
const cancelBtn = await screen.findByTestId('cancel-edit-btn')
|
||||
const cancelBtn = await screen.findByRole('button', { name: 'common.operation.cancel' })
|
||||
await user.click(cancelBtn)
|
||||
|
||||
// Test unmount clearing timer
|
||||
await user.click(screen.getByTestId('edit-btn'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.edit' }))
|
||||
const textbox2 = await screen.findByRole('textbox')
|
||||
fireEvent.compositionStart(textbox2)
|
||||
fireEvent.compositionEnd(textbox2)
|
||||
@ -489,13 +489,13 @@ describe('Question component', () => {
|
||||
const onRegenerate = vi.fn() as unknown as OnRegenerate
|
||||
renderWithProvider(makeItem(), onRegenerate)
|
||||
|
||||
await user.click(screen.getByTestId('edit-btn'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.edit' }))
|
||||
const textbox = await screen.findByRole('textbox')
|
||||
|
||||
fireEvent.compositionStart(textbox)
|
||||
fireEvent.compositionEnd(textbox) // starts timer
|
||||
|
||||
const saveBtn = screen.getByTestId('save-edit-btn')
|
||||
const saveBtn = screen.getByRole('button', { name: 'common.operation.save' })
|
||||
await user.click(saveBtn) // handleResend clears timer
|
||||
|
||||
expect(onRegenerate).toHaveBeenCalled()
|
||||
@ -661,14 +661,14 @@ describe('Question component', () => {
|
||||
it('should hide edit button when enableEdit is explicitly true', () => {
|
||||
renderWithProvider(makeItem(), vi.fn() as unknown as OnRegenerate, { enableEdit: true })
|
||||
|
||||
expect(screen.getByTestId('edit-btn')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('copy-btn')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'common.operation.edit' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'common.operation.copy' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show copy button always regardless of enableEdit setting', () => {
|
||||
renderWithProvider(makeItem(), vi.fn() as unknown as OnRegenerate, { enableEdit: false })
|
||||
|
||||
expect(screen.getByTestId('copy-btn')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'common.operation.copy' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render content switch when no siblings exist', () => {
|
||||
@ -687,7 +687,7 @@ describe('Question component', () => {
|
||||
const user = userEvent.setup()
|
||||
renderWithProvider(makeItem())
|
||||
|
||||
await user.click(screen.getByTestId('edit-btn'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.edit' }))
|
||||
const textbox = await screen.findByRole('textbox')
|
||||
|
||||
expect(textbox).toHaveValue('This is the question content')
|
||||
@ -713,7 +713,7 @@ describe('Question component', () => {
|
||||
|
||||
const { container } = renderWithProvider(makeItem({ message_files: files }))
|
||||
|
||||
await user.click(screen.getByTestId('edit-btn'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.edit' }))
|
||||
|
||||
// FileList should be visible in edit mode with mb-3 margin
|
||||
expect(screen.getByText(/test.txt/i)).toBeInTheDocument()
|
||||
@ -769,7 +769,7 @@ describe('Question component', () => {
|
||||
const onRegenerate = vi.fn() as unknown as OnRegenerate
|
||||
renderWithProvider(makeItem(), onRegenerate)
|
||||
|
||||
await userEvent.click(screen.getByTestId('edit-btn'))
|
||||
await userEvent.click(screen.getByRole('button', { name: 'common.operation.edit' }))
|
||||
const textbox = await screen.findByRole('textbox')
|
||||
|
||||
// Rapid composition cycles
|
||||
@ -792,7 +792,7 @@ describe('Question component', () => {
|
||||
const onRegenerate = vi.fn() as unknown as OnRegenerate
|
||||
renderWithProvider(makeItem(), onRegenerate)
|
||||
|
||||
await user.click(screen.getByTestId('edit-btn'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.edit' }))
|
||||
const textbox = await screen.findByRole('textbox')
|
||||
|
||||
await user.clear(textbox)
|
||||
@ -821,7 +821,7 @@ describe('Question component', () => {
|
||||
const item = makeItem({ message_files: files })
|
||||
renderWithProvider(item, onRegenerate)
|
||||
|
||||
await user.click(screen.getByTestId('edit-btn'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.edit' }))
|
||||
const textbox = await screen.findByRole('textbox')
|
||||
|
||||
await user.clear(textbox)
|
||||
@ -842,24 +842,24 @@ describe('Question component', () => {
|
||||
renderWithProvider(makeItem())
|
||||
|
||||
// First edit cycle
|
||||
await user.click(screen.getByTestId('edit-btn'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.edit' }))
|
||||
let textbox = await screen.findByRole('textbox')
|
||||
fireEvent.compositionStart(textbox)
|
||||
fireEvent.compositionEnd(textbox)
|
||||
|
||||
// Cancel and re-edit
|
||||
let cancelBtn = await screen.findByTestId('cancel-edit-btn')
|
||||
let cancelBtn = await screen.findByRole('button', { name: 'common.operation.cancel' })
|
||||
await user.click(cancelBtn)
|
||||
|
||||
// Second edit cycle
|
||||
await user.click(screen.getByTestId('edit-btn'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.edit' }))
|
||||
textbox = await screen.findByRole('textbox')
|
||||
expect(textbox).toHaveValue('This is the question content')
|
||||
|
||||
fireEvent.compositionStart(textbox)
|
||||
fireEvent.compositionEnd(textbox)
|
||||
|
||||
cancelBtn = await screen.findByTestId('cancel-edit-btn')
|
||||
cancelBtn = await screen.findByRole('button', { name: 'common.operation.cancel' })
|
||||
await user.click(cancelBtn)
|
||||
|
||||
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
|
||||
@ -875,7 +875,7 @@ describe('Question component', () => {
|
||||
expect(contentContainer).toHaveClass('rounded-2xl')
|
||||
expect(contentContainer).toHaveClass('bg-background-gradient-bg-fill-chat-bubble-bg-3')
|
||||
|
||||
await user.click(screen.getByTestId('edit-btn'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.edit' }))
|
||||
|
||||
// Edit mode classes
|
||||
expect(contentContainer).toHaveClass('rounded-[24px]')
|
||||
@ -918,8 +918,8 @@ describe('Question component', () => {
|
||||
</ChatContextProvider>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByTestId('edit-btn'))
|
||||
await user.click(screen.getByTestId('save-edit-btn'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.edit' }))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.save' }))
|
||||
// Should not throw
|
||||
})
|
||||
|
||||
@ -969,7 +969,7 @@ describe('Question component', () => {
|
||||
it('should clear timer on unmount when timer is active', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { unmount } = renderWithProvider(makeItem())
|
||||
await user.click(screen.getByTestId('edit-btn'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.edit' }))
|
||||
const textbox = await screen.findByRole('textbox')
|
||||
fireEvent.compositionStart(textbox)
|
||||
fireEvent.compositionEnd(textbox) // starts timer
|
||||
|
||||
@ -219,13 +219,13 @@ describe('Operation', () => {
|
||||
|
||||
it('should show copy and regenerate buttons', () => {
|
||||
renderOperation()
|
||||
expect(screen.getByTestId('copy-btn'))!.toBeInTheDocument()
|
||||
expect(screen.getByTestId('regenerate-btn'))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'operation.copy' }))!.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'operation.regenerate' }))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide regenerate button when noChatInput is true', () => {
|
||||
renderOperation({ ...baseProps, noChatInput: true })
|
||||
expect(screen.queryByTestId('regenerate-btn')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'operation.regenerate' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show TTS button when text_to_speech is enabled', () => {
|
||||
@ -259,7 +259,7 @@ describe('Operation', () => {
|
||||
it('should copy content on copy click', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderOperation()
|
||||
await user.click(screen.getByTestId('copy-btn'))
|
||||
await user.click(screen.getByRole('button', { name: 'operation.copy' }))
|
||||
expect(copy).toHaveBeenCalledWith('Hello world')
|
||||
})
|
||||
|
||||
@ -274,7 +274,7 @@ describe('Operation', () => {
|
||||
],
|
||||
}
|
||||
renderOperation({ ...baseProps, item })
|
||||
await user.click(screen.getByTestId('copy-btn'))
|
||||
await user.click(screen.getByRole('button', { name: 'operation.copy' }))
|
||||
expect(copy).toHaveBeenCalledWith('Hello World')
|
||||
})
|
||||
})
|
||||
@ -283,7 +283,7 @@ describe('Operation', () => {
|
||||
it('should call onRegenerate on regenerate click', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderOperation()
|
||||
await user.click(screen.getByTestId('regenerate-btn'))
|
||||
await user.click(screen.getByRole('button', { name: 'operation.regenerate' }))
|
||||
expect(mockContextValue.onRegenerate).toHaveBeenCalledWith(baseItem)
|
||||
})
|
||||
})
|
||||
@ -299,7 +299,7 @@ describe('Operation', () => {
|
||||
const item = { ...baseItem, humanInputFormDataList: [{}] } as ChatItem
|
||||
renderOperation({ ...baseProps, item })
|
||||
expect(screen.queryByTestId('audio-btn')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('copy-btn')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'operation.copy' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -793,7 +793,7 @@ describe('Operation', () => {
|
||||
const user = userEvent.setup()
|
||||
const item: ChatItem = { ...baseItem, agent_thoughts: [] }
|
||||
renderOperation({ ...baseProps, item })
|
||||
await user.click(screen.getByTestId('copy-btn'))
|
||||
await user.click(screen.getByRole('button', { name: 'operation.copy' }))
|
||||
expect(copy).toHaveBeenCalledWith('Hello world')
|
||||
})
|
||||
|
||||
|
||||
@ -50,7 +50,7 @@ describe('HumanInputForm', () => {
|
||||
expect(contentItems[1])!.toHaveTextContent('{{#$output.field1#}}')
|
||||
expect(contentItems[2])!.toHaveTextContent('Part 2')
|
||||
|
||||
const buttons = screen.getAllByTestId('action-button')
|
||||
const buttons = screen.getAllByRole('button').filter(button => button.textContent !== 'Update')
|
||||
expect(buttons).toHaveLength(4)
|
||||
expect(buttons[0])!.toHaveTextContent('Submit')
|
||||
expect(buttons[1])!.toHaveTextContent('Cancel')
|
||||
|
||||
@ -49,7 +49,6 @@ const HumanInputForm = ({
|
||||
disabled={isSubmitting}
|
||||
variant={getButtonStyle(action.button_style) as ButtonProps['variant']}
|
||||
onClick={() => submit(formToken, action.id, inputs)}
|
||||
data-testid="action-button"
|
||||
>
|
||||
{action.title}
|
||||
</Button>
|
||||
|
||||
@ -328,13 +328,12 @@ function Operation({
|
||||
copy(content)
|
||||
toast.success(t('actionMsg.copySuccessfully', { ns: 'common' }))
|
||||
}}
|
||||
data-testid="copy-btn"
|
||||
>
|
||||
<span aria-hidden="true" className="i-ri-clipboard-line h-4 w-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
{!noChatInput && (
|
||||
<ActionButton aria-label={regenerateLabel} onClick={() => onRegenerate?.(item)} data-testid="regenerate-btn">
|
||||
<ActionButton aria-label={regenerateLabel} onClick={() => onRegenerate?.(item)}>
|
||||
<span aria-hidden="true" className="i-ri-reset-left-line h-4 w-4" />
|
||||
</ActionButton>
|
||||
)}
|
||||
|
||||
@ -312,7 +312,7 @@ describe('ChatInputArea', () => {
|
||||
|
||||
it('should render the send button', () => {
|
||||
render(<ChatInputArea visionConfig={mockVisionConfig} />)
|
||||
expect(screen.getByTestId('send-button')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'common.operation.send' })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -334,7 +334,7 @@ describe('ChatInputArea', () => {
|
||||
const textarea = getTextarea()!
|
||||
|
||||
await user.type(textarea, 'Hello world')
|
||||
await user.click(screen.getByTestId('send-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.send' }))
|
||||
|
||||
expect(onSend).toHaveBeenCalled()
|
||||
expect(textarea).toHaveValue('')
|
||||
@ -448,14 +448,14 @@ describe('ChatInputArea', () => {
|
||||
describe('Voice Input', () => {
|
||||
it('should render the voice input button when enabled', () => {
|
||||
render(<ChatInputArea speechToTextConfig={{ enabled: true }} visionConfig={mockVisionConfig} />)
|
||||
expect(screen.getByTestId('voice-input-button')).toBeTruthy()
|
||||
expect(screen.getByRole('button', { name: 'common.voiceInput.start' })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should handle stop recording in VoiceInput', async () => {
|
||||
const user = userEvent.setup({ delay: null })
|
||||
render(<ChatInputArea speechToTextConfig={{ enabled: true }} visionConfig={mockVisionConfig} />)
|
||||
|
||||
await user.click(screen.getByTestId('voice-input-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.voiceInput.start' }))
|
||||
// Wait for VoiceInput to show speaking
|
||||
await screen.findByText(/voiceInput.speaking/i)
|
||||
const stopBtn = screen.getByTestId('voice-input-stop')
|
||||
@ -473,7 +473,7 @@ describe('ChatInputArea', () => {
|
||||
const user = userEvent.setup({ delay: null })
|
||||
render(<ChatInputArea speechToTextConfig={{ enabled: true }} visionConfig={mockVisionConfig} />)
|
||||
|
||||
await user.click(screen.getByTestId('voice-input-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.voiceInput.start' }))
|
||||
await screen.findByText(/voiceInput.speaking/i)
|
||||
const stopBtn = screen.getByTestId('voice-input-stop')
|
||||
await user.click(stopBtn)
|
||||
@ -493,7 +493,7 @@ describe('ChatInputArea', () => {
|
||||
|
||||
render(<ChatInputArea speechToTextConfig={{ enabled: true }} visionConfig={mockVisionConfig} />)
|
||||
|
||||
await user.click(screen.getByTestId('voice-input-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.voiceInput.start' }))
|
||||
|
||||
// Permission denied should trigger error toast
|
||||
await waitFor(() => {
|
||||
@ -513,7 +513,7 @@ describe('ChatInputArea', () => {
|
||||
|
||||
render(<ChatInputArea speechToTextConfig={{ enabled: true }} visionConfig={mockVisionConfig} />)
|
||||
|
||||
await user.click(screen.getByTestId('voice-input-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.voiceInput.start' }))
|
||||
await screen.findByText(/voiceInput.speaking/i)
|
||||
const stopBtn = screen.getByTestId('voice-input-stop')
|
||||
await user.click(stopBtn)
|
||||
@ -532,7 +532,7 @@ describe('ChatInputArea', () => {
|
||||
const onSend = vi.fn()
|
||||
render(<ChatInputArea onSend={onSend} visionConfig={mockVisionConfig} />)
|
||||
|
||||
await user.click(screen.getByTestId('send-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.send' }))
|
||||
expect(onSend).not.toHaveBeenCalled()
|
||||
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'info' }))
|
||||
})
|
||||
@ -543,7 +543,7 @@ describe('ChatInputArea', () => {
|
||||
render(<ChatInputArea onSend={onSend} isResponding visionConfig={mockVisionConfig} />)
|
||||
|
||||
await user.type(getTextarea()!, 'Hello')
|
||||
await user.click(screen.getByTestId('send-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.send' }))
|
||||
expect(onSend).not.toHaveBeenCalled()
|
||||
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'info' }))
|
||||
})
|
||||
@ -555,7 +555,7 @@ describe('ChatInputArea', () => {
|
||||
|
||||
render(<ChatInputArea onSend={onSend} visionConfig={mockVisionConfig} />)
|
||||
await user.type(getTextarea()!, 'Hello')
|
||||
await user.click(screen.getByTestId('send-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.send' }))
|
||||
|
||||
expect(onSend).not.toHaveBeenCalled()
|
||||
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'info' }))
|
||||
@ -569,7 +569,7 @@ describe('ChatInputArea', () => {
|
||||
|
||||
render(<ChatInputArea onSend={onSend} visionConfig={mockVisionConfig} />)
|
||||
await user.type(getTextarea()!, 'Hello')
|
||||
await user.click(screen.getByTestId('send-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.send' }))
|
||||
|
||||
expect(onSend).toHaveBeenCalledWith('Hello', [completedFile])
|
||||
})
|
||||
@ -586,7 +586,7 @@ describe('ChatInputArea', () => {
|
||||
|
||||
render(<ChatInputArea onSend={onSend} visionConfig={mockVisionConfig} />)
|
||||
await user.type(getTextarea()!, 'Remote test')
|
||||
await user.click(screen.getByTestId('send-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.send' }))
|
||||
|
||||
expect(onSend).toHaveBeenCalledWith('Remote test', [remoteFile])
|
||||
})
|
||||
@ -598,7 +598,7 @@ describe('ChatInputArea', () => {
|
||||
render(<ChatInputArea onSend={onSend} visionConfig={mockVisionConfig} />)
|
||||
|
||||
await user.type(getTextarea()!, 'Validation fail')
|
||||
await user.click(screen.getByTestId('send-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.send' }))
|
||||
|
||||
expect(onSend).not.toHaveBeenCalled()
|
||||
})
|
||||
@ -608,7 +608,7 @@ describe('ChatInputArea', () => {
|
||||
render(<ChatInputArea visionConfig={mockVisionConfig} />)
|
||||
|
||||
await user.type(getTextarea()!, 'No onSend')
|
||||
await user.click(screen.getByTestId('send-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.send' }))
|
||||
// Should not throw
|
||||
})
|
||||
})
|
||||
@ -663,7 +663,7 @@ describe('ChatInputArea', () => {
|
||||
mockIsMultipleLine.value = true
|
||||
render(<ChatInputArea visionConfig={mockVisionConfig} />)
|
||||
// Send button should still be present
|
||||
expect(screen.getByTestId('send-button')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'common.operation.send' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle drag enter event on textarea', () => {
|
||||
|
||||
@ -58,7 +58,8 @@ describe('Operation', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getAllByRole('button')).toHaveLength(2)
|
||||
expect(screen.getByRole('button', { name: 'common.voiceInput.start' })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'common.operation.send' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render voice input button when speechToTextConfig.enabled is false', () => {
|
||||
@ -136,8 +137,7 @@ describe('Operation', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const voiceButton = buttons[0]
|
||||
const voiceButton = screen.getByRole('button', { name: 'common.voiceInput.start' })
|
||||
|
||||
await user.click(voiceButton!)
|
||||
|
||||
@ -157,8 +157,7 @@ describe('Operation', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const voiceButton = buttons[0]
|
||||
const voiceButton = screen.getByRole('button', { name: 'common.voiceInput.start' })
|
||||
|
||||
expect(voiceButton)!.toBeDisabled()
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
} from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { FileUploaderInChatInput } from '@/app/components/base/file-uploader'
|
||||
|
||||
@ -33,6 +34,8 @@ const Operation: FC<OperationProps> = ({
|
||||
onSend,
|
||||
theme,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@ -49,20 +52,20 @@ const Operation: FC<OperationProps> = ({
|
||||
speechToTextConfig?.enabled && (
|
||||
<ActionButton
|
||||
size="l"
|
||||
aria-label={t('voiceInput.start', { ns: 'common' })}
|
||||
disabled={readonly}
|
||||
onClick={onShowVoiceInput}
|
||||
data-testid="voice-input-button"
|
||||
>
|
||||
<RiMicLine className="h-5 w-5" />
|
||||
<RiMicLine className="h-5 w-5" aria-hidden="true" />
|
||||
</ActionButton>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<Button
|
||||
aria-label={t('operation.send', { ns: 'common' })}
|
||||
className="ml-3 w-8 px-0"
|
||||
variant="primary"
|
||||
onClick={readonly ? noop : onSend}
|
||||
data-testid="send-button"
|
||||
style={
|
||||
theme
|
||||
? {
|
||||
@ -71,7 +74,7 @@ const Operation: FC<OperationProps> = ({
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<RiSendPlane2Fill className="h-4 w-4" />
|
||||
<RiSendPlane2Fill className="h-4 w-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -61,6 +61,8 @@ const makeData = (overrides: Partial<Resources> = {}): Resources => ({
|
||||
const openPopup = async (user: ReturnType<typeof userEvent.setup>) => {
|
||||
await user.click(screen.getByTestId('popup-trigger'))
|
||||
}
|
||||
const getDownloadButton = (name = 'report.pdf') => screen.getByRole('button', { name })
|
||||
const queryDownloadButton = (name = 'report.pdf') => screen.queryByRole('button', { name })
|
||||
|
||||
describe('Popup', () => {
|
||||
beforeEach(() => {
|
||||
@ -142,7 +144,7 @@ describe('Popup', () => {
|
||||
|
||||
await openPopup(user)
|
||||
|
||||
expect(screen.getByTestId('popup-download-btn'))!.toBeInTheDocument()
|
||||
expect(getDownloadButton()).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render download button in header for file dataSourceType with dataset_id', async () => {
|
||||
@ -157,7 +159,7 @@ describe('Popup', () => {
|
||||
|
||||
await openPopup(user)
|
||||
|
||||
expect(screen.getByTestId('popup-download-btn'))!.toBeInTheDocument()
|
||||
expect(getDownloadButton()).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render plain document name in header (no button) for notion type', async () => {
|
||||
@ -173,7 +175,7 @@ describe('Popup', () => {
|
||||
|
||||
await openPopup(user)
|
||||
|
||||
expect(screen.queryByTestId('popup-download-btn')).not.toBeInTheDocument()
|
||||
expect(queryDownloadButton('Notion Doc')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render plain document name in header when dataset_id is absent', async () => {
|
||||
@ -188,7 +190,7 @@ describe('Popup', () => {
|
||||
|
||||
await openPopup(user)
|
||||
|
||||
expect(screen.queryByTestId('popup-download-btn')).not.toBeInTheDocument()
|
||||
expect(queryDownloadButton()).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should disable the download button while isDownloading is true', async () => {
|
||||
@ -201,7 +203,7 @@ describe('Popup', () => {
|
||||
|
||||
await openPopup(user)
|
||||
|
||||
expect(screen.getByTestId('popup-download-btn'))!.toBeDisabled()
|
||||
expect(getDownloadButton()).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -457,7 +459,7 @@ describe('Popup', () => {
|
||||
render(<Popup data={makeData({ dataSourceType: 'upload_file' })} />)
|
||||
|
||||
await openPopup(user)
|
||||
await user.click(screen.getByTestId('popup-download-btn'))
|
||||
await user.click(getDownloadButton())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDownloadDocument).toHaveBeenCalledWith({ datasetId: 'ds-1', documentId: 'doc-1' })
|
||||
@ -471,7 +473,7 @@ describe('Popup', () => {
|
||||
render(<Popup data={makeData({ dataSourceType: 'upload_file' })} />)
|
||||
|
||||
await openPopup(user)
|
||||
await user.click(screen.getByTestId('popup-download-btn'))
|
||||
await user.click(getDownloadButton())
|
||||
|
||||
await waitFor(() => expect(mockDownloadDocument).toHaveBeenCalled())
|
||||
expect(mockDownloadUrl).not.toHaveBeenCalled()
|
||||
@ -489,7 +491,7 @@ describe('Popup', () => {
|
||||
|
||||
await openPopup(user)
|
||||
|
||||
expect(screen.queryByTestId('popup-download-btn')).not.toBeInTheDocument()
|
||||
expect(queryDownloadButton('Notion Doc')).not.toBeInTheDocument()
|
||||
expect(mockDownloadDocument).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -502,7 +504,7 @@ describe('Popup', () => {
|
||||
render(<Popup data={makeData({ dataSourceType: 'upload_file' })} />)
|
||||
|
||||
await openPopup(user)
|
||||
await user.click(screen.getByTestId('popup-download-btn'))
|
||||
await user.click(getDownloadButton())
|
||||
|
||||
expect(mockDownloadDocument).not.toHaveBeenCalled()
|
||||
})
|
||||
@ -520,7 +522,7 @@ describe('Popup', () => {
|
||||
)
|
||||
|
||||
await openPopup(user)
|
||||
await user.click(screen.getByTestId('popup-download-btn'))
|
||||
await user.click(getDownloadButton())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDownloadDocument).toHaveBeenCalledWith({ datasetId: 'ds-1', documentId: 'primary-doc-id' })
|
||||
@ -539,7 +541,7 @@ describe('Popup', () => {
|
||||
)
|
||||
|
||||
await openPopup(user)
|
||||
await user.click(screen.getByTestId('popup-download-btn'))
|
||||
await user.click(getDownloadButton())
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDownloadDocument).toHaveBeenCalled()
|
||||
@ -559,7 +561,7 @@ describe('Popup', () => {
|
||||
)
|
||||
|
||||
await openPopup(user)
|
||||
await user.click(screen.getByTestId('popup-download-btn'))
|
||||
await user.click(getDownloadButton())
|
||||
|
||||
expect(mockDownloadDocument).not.toHaveBeenCalled()
|
||||
})
|
||||
@ -741,7 +743,7 @@ describe('Popup', () => {
|
||||
// we check the handler directly if possible, or just the button absence.
|
||||
// Even if the button is rendered (it shouldn't be based on line 71),
|
||||
// we check the handler directly if possible, or just the button absence.
|
||||
expect(screen.queryByTestId('popup-download-btn')).not.toBeInTheDocument()
|
||||
expect(queryDownloadButton()).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should return early if both documentIds are missing', async () => {
|
||||
@ -756,7 +758,7 @@ describe('Popup', () => {
|
||||
/>,
|
||||
)
|
||||
await openPopup(user)
|
||||
const btn = screen.queryByTestId('popup-download-btn')
|
||||
const btn = queryDownloadButton()
|
||||
if (btn) {
|
||||
await user.click(btn)
|
||||
expect(mockDownloadDocument).not.toHaveBeenCalled()
|
||||
@ -774,7 +776,7 @@ describe('Popup', () => {
|
||||
/>,
|
||||
)
|
||||
await openPopup(user)
|
||||
expect(screen.queryByTestId('popup-download-btn')).not.toBeInTheDocument()
|
||||
expect(queryDownloadButton()).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -70,9 +70,8 @@ const Popup: FC<PopupProps> = ({
|
||||
{(data.dataSourceType === 'upload_file' || data.dataSourceType === 'file') && !!data.sources?.[0]?.dataset_id
|
||||
? (
|
||||
<button
|
||||
data-testid="popup-download-btn"
|
||||
type="button"
|
||||
className="cursor-pointer truncate text-text-tertiary hover:underline"
|
||||
className="cursor-pointer truncate border-none bg-transparent p-0 text-left text-text-tertiary hover:underline"
|
||||
onClick={handleDownloadUploadFile}
|
||||
disabled={isDownloading}
|
||||
>
|
||||
|
||||
@ -52,6 +52,8 @@ const Question: FC<QuestionProps> = ({
|
||||
const {
|
||||
onRegenerate,
|
||||
} = useChatContext()
|
||||
const copyLabel = t('operation.copy', { ns: 'common' })
|
||||
const editLabel = t('operation.edit', { ns: 'common' })
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editedContent, setEditedContent] = useState(content)
|
||||
@ -168,17 +170,17 @@ const Question: FC<QuestionProps> = ({
|
||||
style={{ right: contentWidth + 8 }}
|
||||
>
|
||||
<ActionButton
|
||||
data-testid="copy-btn"
|
||||
aria-label={copyLabel}
|
||||
onClick={() => {
|
||||
copy(content)
|
||||
toast.success(t('actionMsg.copySuccessfully', { ns: 'common' }))
|
||||
}}
|
||||
>
|
||||
<div className="i-ri-clipboard-line h-4 w-4" />
|
||||
<div className="i-ri-clipboard-line h-4 w-4" aria-hidden="true" />
|
||||
</ActionButton>
|
||||
{enableEdit && (
|
||||
<ActionButton data-testid="edit-btn" onClick={handleEdit}>
|
||||
<div className="i-ri-edit-line h-4 w-4" />
|
||||
<ActionButton aria-label={editLabel} onClick={handleEdit}>
|
||||
<div className="i-ri-edit-line h-4 w-4" aria-hidden="true" />
|
||||
</ActionButton>
|
||||
)}
|
||||
</div>
|
||||
@ -222,8 +224,8 @@ const Question: FC<QuestionProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button className="min-w-24" onClick={handleCancelEditing} data-testid="cancel-edit-btn">{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Button className="min-w-24" variant="primary" onClick={handleResend} data-testid="save-edit-btn">{t('operation.save', { ns: 'common' })}</Button>
|
||||
<Button className="min-w-24" onClick={handleCancelEditing}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
<Button className="min-w-24" variant="primary" onClick={handleResend}>{t('operation.save', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -153,7 +153,7 @@ describe('EmbeddedChatbot Header', () => {
|
||||
it('should render reset button when allowResetChat is true and conversation exists', () => {
|
||||
render(<Header title="Test Chatbot" allowResetChat={true} />)
|
||||
|
||||
expect(screen.getByTestId('reset-chat-button')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'share.chat.resetChat' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onCreateNewChat when reset button is clicked', async () => {
|
||||
@ -161,7 +161,7 @@ describe('EmbeddedChatbot Header', () => {
|
||||
const onCreateNewChat = vi.fn()
|
||||
render(<Header title="Test Chatbot" allowResetChat={true} onCreateNewChat={onCreateNewChat} />)
|
||||
|
||||
await user.click(screen.getByTestId('reset-chat-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'share.chat.resetChat' }))
|
||||
expect(onCreateNewChat).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -218,7 +218,7 @@ describe('EmbeddedChatbot Header', () => {
|
||||
it('should render mobile reset button when allowed', () => {
|
||||
render(<Header title="Mobile Chatbot" isMobile allowResetChat />)
|
||||
|
||||
expect(screen.getByTestId('mobile-reset-chat-button')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'share.chat.resetChat' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should NOT render mobile reset button when currentConversationId is missing', () => {
|
||||
@ -228,7 +228,7 @@ describe('EmbeddedChatbot Header', () => {
|
||||
} as EmbeddedChatbotContextValue)
|
||||
render(<Header title="Mobile Chatbot" isMobile allowResetChat />)
|
||||
|
||||
expect(screen.queryByTestId('mobile-reset-chat-button')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'share.chat.resetChat' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render ViewFormDropdown in mobile when conditions are met', () => {
|
||||
@ -247,7 +247,7 @@ describe('EmbeddedChatbot Header', () => {
|
||||
|
||||
await dispatchChatbotConfigMessage('https://parent.com', { isToggledByButton: true, isDraggable: false })
|
||||
|
||||
const expandBtn = await screen.findByTestId('mobile-expand-button')
|
||||
const expandBtn = await screen.findByRole('button', { name: 'share.chat.expand' })
|
||||
expect(expandBtn).toBeInTheDocument()
|
||||
|
||||
await user.click(expandBtn)
|
||||
@ -276,7 +276,7 @@ describe('EmbeddedChatbot Header', () => {
|
||||
|
||||
await dispatchChatbotConfigMessage('https://parent.com', { isToggledByButton: true, isDraggable: false })
|
||||
|
||||
const expandBtn = await screen.findByTestId('expand-button')
|
||||
const expandBtn = await screen.findByRole('button', { name: 'share.chat.expand' })
|
||||
expect(expandBtn).toBeInTheDocument()
|
||||
|
||||
await user.click(expandBtn)
|
||||
@ -295,7 +295,7 @@ describe('EmbeddedChatbot Header', () => {
|
||||
await dispatchChatbotConfigMessage('https://parent.com', { isToggledByButton: true, isDraggable: true })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('expand-button')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'share.chat.expand' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -305,12 +305,12 @@ describe('EmbeddedChatbot Header', () => {
|
||||
|
||||
await dispatchChatbotConfigMessage('https://secure.com', { isToggledByButton: true, isDraggable: false })
|
||||
|
||||
await screen.findByTestId('expand-button')
|
||||
await screen.findByRole('button', { name: 'share.chat.expand' })
|
||||
|
||||
await dispatchChatbotConfigMessage('https://malicious.com', { isToggledByButton: false, isDraggable: false })
|
||||
|
||||
// Should still be visible (not hidden by the malicious message)
|
||||
expect(screen.getByTestId('expand-button')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'share.chat.expand' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should ignore non-config messages for origin locking', async () => {
|
||||
@ -327,7 +327,7 @@ describe('EmbeddedChatbot Header', () => {
|
||||
await dispatchChatbotConfigMessage('https://second.com', { isToggledByButton: true, isDraggable: false })
|
||||
|
||||
// Should lock to second.com
|
||||
const expandBtn = await screen.findByTestId('expand-button')
|
||||
const expandBtn = await screen.findByRole('button', { name: 'share.chat.expand' })
|
||||
expect(expandBtn).toBeInTheDocument()
|
||||
})
|
||||
|
||||
|
||||
@ -114,11 +114,15 @@ const Header: FC<IHeaderProps> = ({
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<ActionButton size="l" onClick={handleToggleExpand} data-testid="expand-button">
|
||||
<ActionButton
|
||||
size="l"
|
||||
aria-label={expanded ? t('chat.collapse', { ns: 'share' }) : t('chat.expand', { ns: 'share' })}
|
||||
onClick={handleToggleExpand}
|
||||
>
|
||||
{
|
||||
expanded
|
||||
? <div className="i-ri-collapse-diagonal-2-line h-[18px] w-[18px]" />
|
||||
: <div className="i-ri-expand-diagonal-2-line h-[18px] w-[18px]" />
|
||||
? <div className="i-ri-collapse-diagonal-2-line h-[18px] w-[18px]" aria-hidden="true" />
|
||||
: <div className="i-ri-expand-diagonal-2-line h-[18px] w-[18px]" aria-hidden="true" />
|
||||
}
|
||||
</ActionButton>
|
||||
)}
|
||||
@ -133,8 +137,12 @@ const Header: FC<IHeaderProps> = ({
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<ActionButton size="l" onClick={onCreateNewChat} data-testid="reset-chat-button">
|
||||
<div className="i-ri-reset-left-line h-[18px] w-[18px]" />
|
||||
<ActionButton
|
||||
size="l"
|
||||
aria-label={t('chat.resetChat', { ns: 'share' })}
|
||||
onClick={onCreateNewChat}
|
||||
>
|
||||
<div className="i-ri-reset-left-line h-[18px] w-[18px]" aria-hidden="true" />
|
||||
</ActionButton>
|
||||
)}
|
||||
/>
|
||||
@ -171,11 +179,15 @@ const Header: FC<IHeaderProps> = ({
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<ActionButton size="l" onClick={handleToggleExpand} data-testid="mobile-expand-button">
|
||||
<ActionButton
|
||||
size="l"
|
||||
aria-label={expanded ? t('chat.collapse', { ns: 'share' }) : t('chat.expand', { ns: 'share' })}
|
||||
onClick={handleToggleExpand}
|
||||
>
|
||||
{
|
||||
expanded
|
||||
? <div className={cn('i-ri-collapse-diagonal-2-line h-[18px] w-[18px]', theme?.colorPathOnHeader)} />
|
||||
: <div className={cn('i-ri-expand-diagonal-2-line h-[18px] w-[18px]', theme?.colorPathOnHeader)} />
|
||||
? <div className={cn('i-ri-collapse-diagonal-2-line h-[18px] w-[18px]', theme?.colorPathOnHeader)} aria-hidden="true" />
|
||||
: <div className={cn('i-ri-expand-diagonal-2-line h-[18px] w-[18px]', theme?.colorPathOnHeader)} aria-hidden="true" />
|
||||
}
|
||||
</ActionButton>
|
||||
)}
|
||||
@ -190,8 +202,12 @@ const Header: FC<IHeaderProps> = ({
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<ActionButton size="l" onClick={onCreateNewChat} data-testid="mobile-reset-chat-button">
|
||||
<div className={cn('i-ri-reset-left-line h-[18px] w-[18px]', theme?.colorPathOnHeader)} />
|
||||
<ActionButton
|
||||
size="l"
|
||||
aria-label={t('chat.resetChat', { ns: 'share' })}
|
||||
onClick={onCreateNewChat}
|
||||
>
|
||||
<div className={cn('i-ri-reset-left-line h-[18px] w-[18px]', theme?.colorPathOnHeader)} aria-hidden="true" />
|
||||
</ActionButton>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -56,19 +56,19 @@ describe('InputsFormNode', () => {
|
||||
render(<InputsFormNode collapsed={false} setCollapsed={setCollapsed} />)
|
||||
expect(screen.getByText(/chat.chatSettingsTitle/i)).toBeInTheDocument()
|
||||
expect(screen.getByTestId('mock-inputs-form-content')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('inputs-form-start-chat-button')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'share.chat.startChat' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render collapsed state correctly', () => {
|
||||
render(<InputsFormNode collapsed={true} setCollapsed={setCollapsed} />)
|
||||
expect(screen.getByText(/chat.chatSettingsTitle/i)).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('mock-inputs-form-content')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('inputs-form-edit-button')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'common.operation.edit' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle edit button click', async () => {
|
||||
render(<InputsFormNode collapsed={true} setCollapsed={setCollapsed} />)
|
||||
await user.click(screen.getByTestId('inputs-form-edit-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.edit' }))
|
||||
expect(setCollapsed).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
@ -78,7 +78,7 @@ describe('InputsFormNode', () => {
|
||||
currentConversationId: 'conv-123',
|
||||
} as unknown as any)
|
||||
render(<InputsFormNode collapsed={false} setCollapsed={setCollapsed} />)
|
||||
await user.click(screen.getByTestId('inputs-form-close-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'common.operation.close' }))
|
||||
expect(setCollapsed).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
@ -90,7 +90,7 @@ describe('InputsFormNode', () => {
|
||||
handleStartChat,
|
||||
} as unknown as any)
|
||||
render(<InputsFormNode collapsed={false} setCollapsed={setCollapsed} />)
|
||||
await user.click(screen.getByTestId('inputs-form-start-chat-button'))
|
||||
await user.click(screen.getByRole('button', { name: 'share.chat.startChat' }))
|
||||
expect(handleStartChat).toHaveBeenCalled()
|
||||
expect(setCollapsed).toHaveBeenCalledWith(true)
|
||||
})
|
||||
@ -105,7 +105,7 @@ describe('InputsFormNode', () => {
|
||||
},
|
||||
} as unknown as any)
|
||||
render(<InputsFormNode collapsed={false} setCollapsed={setCollapsed} />)
|
||||
const button = screen.getByTestId('inputs-form-start-chat-button')
|
||||
const button = screen.getByRole('button', { name: 'share.chat.startChat' })
|
||||
expect(button).toHaveStyle({ backgroundColor: '#ff0000' })
|
||||
})
|
||||
|
||||
@ -138,7 +138,7 @@ describe('InputsFormNode', () => {
|
||||
expect(screen.getByTestId('mock-inputs-form-content').parentElement).toHaveClass('p-4')
|
||||
|
||||
// Start chat button container
|
||||
expect(screen.getByTestId('inputs-form-start-chat-button').parentElement).toHaveClass('p-4')
|
||||
expect(screen.getByRole('button', { name: 'share.chat.startChat' }).parentElement).toHaveClass('p-4')
|
||||
|
||||
// Collapsed state mobile styles
|
||||
rerender(<InputsFormNode collapsed={true} setCollapsed={setCollapsed} />)
|
||||
|
||||
@ -56,7 +56,6 @@ const InputsFormNode = ({
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={() => setCollapsed(false)}
|
||||
data-testid="inputs-form-edit-button"
|
||||
>
|
||||
{t('operation.edit', { ns: 'common' })}
|
||||
</Button>
|
||||
@ -67,7 +66,6 @@ const InputsFormNode = ({
|
||||
size="small"
|
||||
variant="ghost"
|
||||
onClick={() => setCollapsed(true)}
|
||||
data-testid="inputs-form-close-button"
|
||||
>
|
||||
{t('operation.close', { ns: 'common' })}
|
||||
</Button>
|
||||
@ -84,7 +82,6 @@ const InputsFormNode = ({
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
onClick={() => handleStartChat(() => setCollapsed(true))}
|
||||
data-testid="inputs-form-start-chat-button"
|
||||
style={
|
||||
themeBuilder?.theme
|
||||
? {
|
||||
|
||||
@ -35,15 +35,15 @@ describe('CopyFeedback', () => {
|
||||
describe('User Interactions', () => {
|
||||
it('calls copy with content when clicked', () => {
|
||||
render(<CopyFeedback content="test content" />)
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button.firstChild as Element)
|
||||
const button = screen.getByRole('button', { name: 'appOverview.overview.appInfo.embedded.copy' })
|
||||
fireEvent.click(button)
|
||||
expect(mockCopy).toHaveBeenCalledWith('test content')
|
||||
})
|
||||
|
||||
it('does not reset on mouse leave (relies on hook timeout)', () => {
|
||||
render(<CopyFeedback content="test content" />)
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.mouseLeave(button.firstChild as Element)
|
||||
const button = screen.getByRole('button', { name: 'appOverview.overview.appInfo.embedded.copy' })
|
||||
fireEvent.mouseLeave(button)
|
||||
expect(mockReset).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@ -57,8 +57,8 @@ describe('CopyFeedbackNew', () => {
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('renders the component', () => {
|
||||
const { container } = render(<CopyFeedbackNew content="test content" />)
|
||||
expect(container.querySelector('.cursor-pointer')).toBeInTheDocument()
|
||||
render(<CopyFeedbackNew content="test content" />)
|
||||
expect(screen.getByRole('button', { name: 'appOverview.overview.appInfo.embedded.copy' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders with custom className', () => {
|
||||
@ -82,16 +82,14 @@ describe('CopyFeedbackNew', () => {
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('calls copy with content when clicked', () => {
|
||||
const { container } = render(<CopyFeedbackNew content="test content" />)
|
||||
const clickableArea = container.querySelector('.cursor-pointer')!.firstChild as HTMLElement
|
||||
fireEvent.click(clickableArea)
|
||||
render(<CopyFeedbackNew content="test content" />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'appOverview.overview.appInfo.embedded.copy' }))
|
||||
expect(mockCopy).toHaveBeenCalledWith('test content')
|
||||
})
|
||||
|
||||
it('does not reset on mouse leave (relies on hook timeout)', () => {
|
||||
const { container } = render(<CopyFeedbackNew content="test content" />)
|
||||
const clickableArea = container.querySelector('.cursor-pointer')!.firstChild as HTMLElement
|
||||
fireEvent.mouseLeave(clickableArea)
|
||||
render(<CopyFeedbackNew content="test content" />)
|
||||
fireEvent.mouseLeave(screen.getByRole('button', { name: 'appOverview.overview.appInfo.embedded.copy' }))
|
||||
expect(mockReset).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -38,11 +38,9 @@ const CopyFeedback = ({ content }: Props) => {
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<ActionButton>
|
||||
<div onClick={handleCopy}>
|
||||
{copied && <RiClipboardFill className="h-4 w-4" />}
|
||||
{!copied && <RiClipboardLine className="h-4 w-4" />}
|
||||
</div>
|
||||
<ActionButton aria-label={safeText} onClick={handleCopy}>
|
||||
{copied && <RiClipboardFill className="h-4 w-4" aria-hidden="true" />}
|
||||
{!copied && <RiClipboardLine className="h-4 w-4" aria-hidden="true" />}
|
||||
</ActionButton>
|
||||
)}
|
||||
/>
|
||||
@ -73,15 +71,17 @@ export const CopyFeedbackNew = ({ content, className }: Pick<Props, 'className'
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={(
|
||||
<div
|
||||
className={`h-8 w-8 cursor-pointer rounded-lg hover:bg-components-button-ghost-bg-hover ${className ?? ''}`}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={safeText}
|
||||
className={`h-8 w-8 cursor-pointer rounded-lg border-none bg-transparent p-0 hover:bg-components-button-ghost-bg-hover focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden ${className ?? ''}`}
|
||||
onClick={handleCopy}
|
||||
>
|
||||
<div
|
||||
onClick={handleCopy}
|
||||
className={`h-full w-full ${copyStyle.copyIcon} ${copied ? copyStyle.copied : ''}`}
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
<TooltipContent>
|
||||
|
||||
@ -21,29 +21,24 @@ describe('copy icon component', () => {
|
||||
|
||||
it('renders normally', () => {
|
||||
render(<CopyIcon content="this is some test content for the copy icon component" />)
|
||||
const icon = screen.getByTestId('copy-icon')
|
||||
expect(icon).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'appOverview.overview.appInfo.embedded.copy' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows copy check icon when copied', () => {
|
||||
copied = true
|
||||
render(<CopyIcon content="this is some test content for the copy icon component" />)
|
||||
const icon = screen.getByTestId('copied-icon')
|
||||
expect(icon).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'appOverview.overview.appInfo.embedded.copied' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('handles copy when clicked', () => {
|
||||
render(<CopyIcon content="this is some test content for the copy icon component" />)
|
||||
const icon = screen.getByTestId('copy-icon')
|
||||
fireEvent.click(icon as Element)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'appOverview.overview.appInfo.embedded.copy' }))
|
||||
expect(copy).toBeCalledTimes(1)
|
||||
})
|
||||
|
||||
it('resets on mouse leave', () => {
|
||||
render(<CopyIcon content="this is some test content for the copy icon component" />)
|
||||
const icon = screen.getByTestId('copy-icon')
|
||||
const div = icon?.parentElement as HTMLElement
|
||||
fireEvent.mouseLeave(div)
|
||||
fireEvent.mouseLeave(screen.getByRole('button', { name: 'appOverview.overview.appInfo.embedded.copy' }))
|
||||
expect(reset).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@ -36,8 +36,8 @@ const CopyIcon = ({ content }: Props) => {
|
||||
onMouseLeave={reset}
|
||||
>
|
||||
{!copied
|
||||
? (<span aria-hidden className="i-custom-vender-line-files-copy h-3.5 w-3.5" data-testid="copy-icon" />)
|
||||
: (<span aria-hidden className="i-custom-vender-line-files-copy-check h-3.5 w-3.5" data-testid="copied-icon" />)}
|
||||
? (<span aria-hidden className="i-custom-vender-line-files-copy h-3.5 w-3.5" />)
|
||||
: (<span aria-hidden className="i-custom-vender-line-files-copy-check h-3.5 w-3.5" />)}
|
||||
</button>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -513,10 +513,7 @@ describe('DatePicker', () => {
|
||||
// Open year/month picker
|
||||
fireEvent.click(screen.getByText(/2024/))
|
||||
|
||||
// The header in year/month view shows selected month/year with an up arrow
|
||||
// Clicking it closes the year/month picker
|
||||
const headerButtons = screen.getAllByRole('button')
|
||||
fireEvent.click(headerButtons[0]!) // First button in year/month view is the header
|
||||
fireEvent.click(screen.getByRole('button', { name: /time\.months\.June 2024/ }))
|
||||
|
||||
// Should return to date view
|
||||
expect(screen.getAllByText(/daysInWeek/).length).toBeGreaterThan(0)
|
||||
|
||||
@ -228,7 +228,14 @@ const DatePicker = ({
|
||||
placeholder={placeholderDate}
|
||||
/>
|
||||
<span className={cn('i-ri-calendar-line h-4 w-4 shrink-0 text-text-quaternary', isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary', (displayValue || (isOpen && selectedDate)) && 'group-hover:hidden')} />
|
||||
<span className={cn('i-ri-close-circle-fill hidden h-4 w-4 shrink-0 text-text-quaternary', (displayValue || (isOpen && selectedDate)) && 'group-hover:inline-block hover:text-text-secondary')} onClick={handleClear} data-testid="date-picker-clear-button" />
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.clear', { ns: 'common' })}
|
||||
className={cn('hidden h-4 w-4 shrink-0 border-none bg-transparent p-0 text-text-quaternary hover:text-text-secondary focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden', (displayValue || (isOpen && selectedDate)) && 'group-hover:inline-block')}
|
||||
onClick={handleClear}
|
||||
>
|
||||
<span className="i-ri-close-circle-fill h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -227,7 +227,14 @@ const TimePicker = ({
|
||||
<TimezoneLabel timezone={timezone} inline className="shrink-0 text-xs select-none" />
|
||||
)}
|
||||
<span className={cn('i-ri-time-line h-4 w-4 shrink-0 text-text-quaternary', isOpen ? 'text-text-secondary' : 'group-hover:text-text-secondary', (displayValue || (isOpen && selectedTime)) && !notClearable && 'group-hover:hidden')} />
|
||||
<span className={cn('i-ri-close-circle-fill hidden h-4 w-4 shrink-0 text-text-quaternary', (displayValue || (isOpen && selectedTime)) && !notClearable && 'group-hover:inline-block hover:text-text-secondary')} role="button" aria-label={t('operation.clear', { ns: 'common' })} onClick={handleClear} />
|
||||
<button
|
||||
type="button"
|
||||
className={cn('hidden h-4 w-4 shrink-0 border-none bg-transparent p-0 text-text-quaternary hover:text-text-secondary focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden', (displayValue || (isOpen && selectedTime)) && !notClearable && 'group-hover:inline-block')}
|
||||
aria-label={t('operation.clear', { ns: 'common' })}
|
||||
onClick={handleClear}
|
||||
>
|
||||
<span className="i-ri-close-circle-fill h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
@ -57,6 +57,7 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
|
||||
|
||||
const [searchedEmojis, setSearchedEmojis] = useState<string[]>([])
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
const styleColorsLabelId = React.useId()
|
||||
|
||||
React.useEffect(() => {
|
||||
if (selectedEmoji) {
|
||||
@ -101,18 +102,20 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
|
||||
<div className="grid h-full w-full grid-cols-8 gap-1">
|
||||
{searchedEmojis.map((emoji: string, index: number) => {
|
||||
return (
|
||||
<div
|
||||
<button
|
||||
type="button"
|
||||
key={`emoji-search-${index}`}
|
||||
className="inline-flex h-10 w-10 items-center justify-center rounded-lg"
|
||||
aria-label={emoji}
|
||||
className="inline-flex h-10 w-10 items-center justify-center rounded-lg border-none bg-transparent p-0"
|
||||
onClick={() => {
|
||||
setSelectedEmoji(emoji)
|
||||
setShowStyleColors(true)
|
||||
}}
|
||||
>
|
||||
<div className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg p-1 ring-components-input-border-hover ring-offset-1 hover:ring-1" data-testid={`emoji-search-result-${emoji}`}>
|
||||
<span className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg p-1 ring-components-input-border-hover ring-offset-1 hover:ring-1">
|
||||
<em-emoji id={emoji} />
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
@ -127,18 +130,20 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
|
||||
<div className="grid h-full w-full grid-cols-8 gap-1">
|
||||
{category.emojis.map((emoji, index: number) => {
|
||||
return (
|
||||
<div
|
||||
<button
|
||||
type="button"
|
||||
key={`emoji-${index}`}
|
||||
className="inline-flex h-10 w-10 items-center justify-center rounded-lg"
|
||||
aria-label={emoji}
|
||||
className="inline-flex h-10 w-10 items-center justify-center rounded-lg border-none bg-transparent p-0"
|
||||
onClick={() => {
|
||||
setSelectedEmoji(emoji)
|
||||
setShowStyleColors(true)
|
||||
}}
|
||||
>
|
||||
<div className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg p-1 ring-components-input-border-hover ring-offset-1 hover:ring-1" data-testid={`emoji-container-${emoji}`}>
|
||||
<span className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg p-1 ring-components-input-border-hover ring-offset-1 hover:ring-1">
|
||||
<em-emoji id={emoji} />
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
|
||||
@ -150,21 +155,40 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
|
||||
|
||||
{/* Color Select */}
|
||||
<div className={cn('flex items-center justify-between p-3 pb-0')}>
|
||||
<p className="mb-2 system-xs-medium-uppercase text-text-primary">Choose Style</p>
|
||||
<p id={styleColorsLabelId} className="mb-2 system-xs-medium-uppercase text-text-primary">Choose Style</p>
|
||||
{showStyleColors
|
||||
? <span className="i-heroicons-chevron-down h-4 w-4 cursor-pointer text-text-quaternary" onClick={() => setShowStyleColors(!showStyleColors)} data-testid="toggle-colors" />
|
||||
: <span className="i-heroicons-chevron-up h-4 w-4 cursor-pointer text-text-quaternary" onClick={() => setShowStyleColors(!showStyleColors)} data-testid="toggle-colors" />}
|
||||
? (
|
||||
<button
|
||||
type="button"
|
||||
aria-labelledby={styleColorsLabelId}
|
||||
aria-expanded="true"
|
||||
className="i-heroicons-chevron-down h-4 w-4 cursor-pointer border-none bg-transparent p-0 text-text-quaternary"
|
||||
onClick={() => setShowStyleColors(!showStyleColors)}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<button
|
||||
type="button"
|
||||
aria-labelledby={styleColorsLabelId}
|
||||
aria-expanded="false"
|
||||
className="i-heroicons-chevron-up h-4 w-4 cursor-pointer border-none bg-transparent p-0 text-text-quaternary"
|
||||
onClick={() => setShowStyleColors(!showStyleColors)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{showStyleColors && (
|
||||
<div className="grid w-full grid-cols-8 gap-1 px-3">
|
||||
{backgroundColors.map((color) => {
|
||||
return (
|
||||
<div
|
||||
<button
|
||||
type="button"
|
||||
key={color}
|
||||
aria-label={color}
|
||||
className={
|
||||
cn(
|
||||
'cursor-pointer',
|
||||
'ring-offset-1 hover:ring-1',
|
||||
'border-none bg-transparent p-0',
|
||||
'ring-components-input-border-hover ring-offset-1 hover:ring-1',
|
||||
'inline-flex h-10 w-10 items-center justify-center rounded-lg',
|
||||
color === selectedBackground ? 'ring-1 ring-components-input-border-hover' : '',
|
||||
)
|
||||
@ -173,15 +197,15 @@ const EmojiPickerInner: FC<IEmojiPickerInnerProps> = ({
|
||||
setSelectedBackground(color)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
<span
|
||||
className={cn(
|
||||
'flex h-8 w-8 items-center justify-center rounded-lg p-1',
|
||||
)}
|
||||
style={{ background: color }}
|
||||
>
|
||||
{selectedEmoji !== '' && <em-emoji id={selectedEmoji} />}
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
@ -75,10 +75,10 @@ describe('EmojiPickerInner', () => {
|
||||
|
||||
it('updates selected emoji and calls onSelect when an emoji is clicked', async () => {
|
||||
render(<EmojiPickerInner onSelect={mockOnSelect} />)
|
||||
const emojiContainers = screen.getAllByTestId(/^emoji-container-/)
|
||||
const emojiButton = screen.getByRole('button', { name: 'rabbit' })
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(emojiContainers[0]!)
|
||||
fireEvent.click(emojiButton)
|
||||
})
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith('rabbit', expect.any(String))
|
||||
@ -89,7 +89,7 @@ describe('EmojiPickerInner', () => {
|
||||
|
||||
expect(screen.queryByText('#FFEAD5')).not.toBeInTheDocument()
|
||||
|
||||
const toggleButton = screen.getByTestId('toggle-colors')
|
||||
const toggleButton = screen.getByRole('button', { name: 'Choose Style' })
|
||||
expect(toggleButton)!.toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
@ -104,21 +104,21 @@ describe('EmojiPickerInner', () => {
|
||||
it('updates background color and calls onSelect when a color is clicked', async () => {
|
||||
render(<EmojiPickerInner onSelect={mockOnSelect} />)
|
||||
|
||||
const toggleButton = screen.getByTestId('toggle-colors')
|
||||
const toggleButton = screen.getByRole('button', { name: 'Choose Style' })
|
||||
await act(async () => {
|
||||
fireEvent.click(toggleButton!)
|
||||
})
|
||||
|
||||
const emojiContainers = screen.getAllByTestId(/^emoji-container-/)
|
||||
const emojiButton = screen.getByRole('button', { name: 'rabbit' })
|
||||
await act(async () => {
|
||||
fireEvent.click(emojiContainers[0]!)
|
||||
fireEvent.click(emojiButton)
|
||||
})
|
||||
|
||||
mockOnSelect.mockClear()
|
||||
|
||||
const colorOptions = document.querySelectorAll('[style^="background:"]')
|
||||
const colorOptions = screen.getAllByRole('button', { name: /^#/ })
|
||||
await act(async () => {
|
||||
fireEvent.click(colorOptions[1]!.parentElement!)
|
||||
fireEvent.click(colorOptions[1]!)
|
||||
})
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith('rabbit', '#E4FBCC')
|
||||
@ -134,9 +134,9 @@ describe('EmojiPickerInner', () => {
|
||||
|
||||
await screen.findByText('Search')
|
||||
|
||||
const searchEmojis = screen.getAllByTestId(/^emoji-search-result-/)
|
||||
const searchEmoji = screen.getByRole('button', { name: 'dog' })
|
||||
await act(async () => {
|
||||
fireEvent.click(searchEmojis![0]!)
|
||||
fireEvent.click(searchEmoji)
|
||||
})
|
||||
|
||||
expect(mockOnSelect).toHaveBeenCalledWith('dog', expect.any(String))
|
||||
@ -145,7 +145,7 @@ describe('EmojiPickerInner', () => {
|
||||
it('toggles style colors display back and forth', async () => {
|
||||
render(<EmojiPickerInner onSelect={mockOnSelect} />)
|
||||
|
||||
const toggleButton = screen.getByTestId('toggle-colors')
|
||||
const toggleButton = screen.getByRole('button', { name: 'Choose Style' })
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(toggleButton!)
|
||||
@ -153,7 +153,7 @@ describe('EmojiPickerInner', () => {
|
||||
expect(screen.getByText('Choose Style'))!.toBeInTheDocument()
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByTestId('toggle-colors')!) // It should be the other icon now
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Choose Style' }))
|
||||
})
|
||||
expect(screen.queryByText('#FFEAD5')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -81,10 +81,8 @@ describe('EmojiPicker', () => {
|
||||
)
|
||||
})
|
||||
|
||||
const emojiWrappers = screen.getAllByTestId(/^emoji-container-/)
|
||||
expect(emojiWrappers.length).toBeGreaterThan(0)
|
||||
await act(async () => {
|
||||
fireEvent.click(emojiWrappers[0]!)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'emoji1' }))
|
||||
})
|
||||
|
||||
const okButton = screen.getByText(/OK/i)
|
||||
|
||||
@ -146,7 +146,7 @@ describe('OpeningSettingModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const closeButton = screen.getByTestId('close-modal')
|
||||
const closeButton = screen.getByRole('button', { name: 'common.operation.close' })
|
||||
await userEvent.click(closeButton)
|
||||
|
||||
expect(onCancel).toHaveBeenCalled()
|
||||
@ -162,7 +162,7 @@ describe('OpeningSettingModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const closeButton = screen.getByTestId('close-modal')
|
||||
const closeButton = screen.getByRole('button', { name: 'common.operation.close' })
|
||||
closeButton.focus()
|
||||
await userEvent.keyboard('{Enter}')
|
||||
|
||||
@ -179,9 +179,9 @@ describe('OpeningSettingModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const closeButton = screen.getByTestId('close-modal')
|
||||
const closeButton = screen.getByRole('button', { name: 'common.operation.close' })
|
||||
closeButton.focus()
|
||||
fireEvent.keyDown(closeButton, { key: ' ' })
|
||||
await userEvent.keyboard(' ')
|
||||
|
||||
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@ -196,7 +196,7 @@ describe('OpeningSettingModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const closeButton = screen.getByTestId('close-modal')
|
||||
const closeButton = screen.getByRole('button', { name: 'common.operation.close' })
|
||||
closeButton.focus()
|
||||
fireEvent.keyDown(closeButton, { key: 'Escape' })
|
||||
|
||||
|
||||
@ -227,21 +227,14 @@ const OpeningSettingModal = ({
|
||||
<DialogContent className="mt-14 w-[640px] max-w-none rounded-2xl bg-components-panel-bg-blur p-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="title-2xl-semi-bold text-text-primary">{t('feature.conversationOpener.title', { ns: 'appDebug' })}</div>
|
||||
<div
|
||||
className="cursor-pointer p-1"
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.close', { ns: 'common' })}
|
||||
className="cursor-pointer border-none bg-transparent p-1 focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={onCancel}
|
||||
data-testid="close-modal"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
onCancel()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="i-ri-close-line h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
<span className="i-ri-close-line h-4 w-4 text-text-tertiary" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mb-8 space-y-4">
|
||||
<div
|
||||
|
||||
@ -120,7 +120,7 @@ describe('SettingContent', () => {
|
||||
const onClose = vi.fn()
|
||||
renderWithProvider({ onClose })
|
||||
|
||||
const closeIconButton = screen.getByTestId('close-setting-modal')
|
||||
const closeIconButton = screen.getByRole('button', { name: 'common.operation.close' })
|
||||
expect(closeIconButton).toBeInTheDocument()
|
||||
if (!closeIconButton)
|
||||
throw new Error('Close icon button should exist')
|
||||
@ -134,7 +134,7 @@ describe('SettingContent', () => {
|
||||
const onClose = vi.fn()
|
||||
renderWithProvider({ onClose })
|
||||
|
||||
const closeIconButton = screen.getByTestId('close-setting-modal')
|
||||
const closeIconButton = screen.getByRole('button', { name: 'common.operation.close' })
|
||||
closeIconButton.focus()
|
||||
await userEvent.keyboard('{Enter}')
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
@ -144,9 +144,9 @@ describe('SettingContent', () => {
|
||||
const onClose = vi.fn()
|
||||
renderWithProvider({ onClose })
|
||||
|
||||
const closeIconButton = screen.getByTestId('close-setting-modal')
|
||||
const closeIconButton = screen.getByRole('button', { name: 'common.operation.close' })
|
||||
closeIconButton.focus()
|
||||
fireEvent.keyDown(closeIconButton, { key: ' ' })
|
||||
await userEvent.keyboard(' ')
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
@ -154,7 +154,7 @@ describe('SettingContent', () => {
|
||||
const onClose = vi.fn()
|
||||
renderWithProvider({ onClose })
|
||||
|
||||
const closeIconButton = screen.getByTestId('close-setting-modal')
|
||||
const closeIconButton = screen.getByRole('button', { name: 'common.operation.close' })
|
||||
closeIconButton.focus()
|
||||
fireEvent.keyDown(closeIconButton, { key: 'Escape' })
|
||||
|
||||
|
||||
@ -58,21 +58,14 @@ const SettingContent = ({
|
||||
<>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="system-xl-semibold text-text-primary">{!imageUpload ? t('feature.fileUpload.modalTitle', { ns: 'appDebug' }) : t('feature.imageUpload.modalTitle', { ns: 'appDebug' })}</div>
|
||||
<div
|
||||
className="cursor-pointer p-1"
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.close', { ns: 'common' })}
|
||||
className="cursor-pointer border-none bg-transparent p-1 focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={onClose}
|
||||
data-testid="close-setting-modal"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
onClose()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="i-ri-close-line h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
<span className="i-ri-close-line h-4 w-4 text-text-tertiary" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<FileUploadSetting
|
||||
isMultiple
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { ModerationConfig } from '@/models/debug'
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import * as i18n from 'react-i18next'
|
||||
import ModerationSettingModal from '../moderation-setting-modal'
|
||||
|
||||
@ -181,10 +182,10 @@ describe('ModerationSettingModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const closeButton = document.querySelector('div[role="button"][tabindex="0"]') as HTMLElement
|
||||
expect(closeButton)!.toBeInTheDocument()
|
||||
const user = userEvent.setup()
|
||||
const closeButton = screen.getByRole('button', { name: 'common.operation.close' })
|
||||
closeButton.focus()
|
||||
fireEvent.keyDown(closeButton, { key: 'Enter' })
|
||||
await user.keyboard('{Enter}')
|
||||
|
||||
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@ -199,10 +200,10 @@ describe('ModerationSettingModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const closeButton = document.querySelector('div[role="button"][tabindex="0"]') as HTMLElement
|
||||
expect(closeButton)!.toBeInTheDocument()
|
||||
const user = userEvent.setup()
|
||||
const closeButton = screen.getByRole('button', { name: 'common.operation.close' })
|
||||
closeButton.focus()
|
||||
fireEvent.keyDown(closeButton, { key: ' ' })
|
||||
await user.keyboard(' ')
|
||||
|
||||
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@ -217,8 +218,7 @@ describe('ModerationSettingModal', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
const closeButton = document.querySelector('div[role="button"][tabindex="0"]') as HTMLElement
|
||||
expect(closeButton)!.toBeInTheDocument()
|
||||
const closeButton = screen.getByRole('button', { name: 'common.operation.close' })
|
||||
closeButton.focus()
|
||||
fireEvent.keyDown(closeButton, { key: 'Escape' })
|
||||
|
||||
|
||||
@ -230,20 +230,14 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="title-2xl-semi-bold text-text-primary">{t('feature.moderation.modal.title', { ns: 'appDebug' })}</div>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="cursor-pointer p-1"
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('operation.close', { ns: 'common' })}
|
||||
className="cursor-pointer border-none bg-transparent p-1 focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={onCancel}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
onCancel()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="i-ri-close-line h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
<span className="i-ri-close-line h-4 w-4 text-text-tertiary" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="py-2">
|
||||
<div className="text-sm leading-9 font-medium text-text-primary">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user