Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -4519,9 +4519,6 @@
}
},
"web/app/components/workflow/nodes/tool/components/tool-form/item.tsx": {
"no-restricted-imports": {
"count": 1
},
"ts/no-explicit-any": {
"count": 1
}
Expand Down
10 changes: 10 additions & 0 deletions web/__tests__/apps/app-card-operations-flow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ vi.mock('@/next/navigation', () => ({
}),
}))

vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: () => ({
data: [],
}),
}
})

// Mock headless UI Popover so it renders content without transition
vi.mock('@headlessui/react', async () => {
const actual = await vi.importActual<typeof import('@headlessui/react')>('@headlessui/react')
Expand Down
18 changes: 13 additions & 5 deletions web/__tests__/apps/app-list-browsing-flow.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { ReactElement, ReactNode } from 'react'
*/
import type { AppListResponse } from '@/models/app'
import type { App } from '@/types/app'
import { fireEvent, render, screen } from '@testing-library/react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createSystemFeaturesWrapper } from '@/__tests__/utils/mock-system-features'
import List from '@/app/components/apps/list'
Expand Down Expand Up @@ -92,6 +92,9 @@ vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: () => ({
data: [],
}),
useInfiniteQuery: () => ({
data: { pages: mockPages },
isLoading: mockIsLoading,
Expand Down Expand Up @@ -360,13 +363,18 @@ describe('App List Browsing Flow', () => {
expect(input).toBeInTheDocument()
})

it('should allow typing in search input', () => {
it('should update search query when typing in search input', async () => {
mockPages = [createPage([createMockApp()])]
renderList()
const { onUrlUpdate } = renderList()

const input = document.querySelector('input')!
const input = screen.getByPlaceholderText('common.operation.search')
fireEvent.change(input, { target: { value: 'test search' } })
expect(input.value).toBe('test search')

await waitFor(() => {
expect(onUrlUpdate).toHaveBeenCalled()
})
const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0]
expect(lastCall.searchParams.get('keywords')).toBe('test search')
})
})

Expand Down
9 changes: 9 additions & 0 deletions web/app/components/apps/__tests__/app-card.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,15 @@ describe('AppCard', () => {
expect(screen.getByTestId('dropdown-menu')).toHaveAttribute('data-modal', 'false')
})

it('should reveal operations trigger when card receives keyboard focus', () => {
render(<AppCard app={mockApp} />)
const operationsTriggerWrapper = screen.getByTestId('dropdown-menu-trigger').closest('.absolute')

expect(operationsTriggerWrapper).toHaveClass('group-focus-within:pointer-events-auto')
expect(operationsTriggerWrapper).toHaveClass('group-focus-within:opacity-100')
expect(screen.getByTestId('dropdown-menu-trigger')).toHaveClass('focus-visible:ring-1')
})

it('should show edit option when dropdown menu is opened', async () => {
render(<AppCard app={mockApp} />)

Expand Down
6 changes: 3 additions & 3 deletions web/app/components/apps/app-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ const AppCard = ({ app, onlineUsers = [], onRefresh, onOpenTagManagement = () =>
e.preventDefault()
getRedirection(isCurrentWorkspaceEditor, app, push)
}}
className="group relative col-span-1 inline-flex h-[160px] cursor-pointer flex-col rounded-xl border border-solid border-components-card-border bg-components-card-bg shadow-sm transition-all duration-200 ease-in-out hover:shadow-lg"
className="group relative col-span-1 inline-flex h-[160px] cursor-pointer flex-col rounded-xl border border-solid border-components-card-border bg-components-card-bg shadow-sm transition-shadow duration-200 ease-in-out hover:shadow-lg"
>
<div className="flex h-[66px] shrink-0 grow-0 items-center gap-3 px-[14px] pt-[14px] pb-3">
<div className="relative shrink-0">
Expand Down Expand Up @@ -524,7 +524,7 @@ const AppCard = ({ app, onlineUsers = [], onRefresh, onOpenTagManagement = () =>
'absolute top-1/2 right-[6px] flex -translate-y-1/2 items-center transition-opacity',
isOperationsMenuOpen
? 'pointer-events-auto opacity-100'
: 'pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100',
: 'pointer-events-none opacity-0 group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100',
)}
>
<div className="mx-1 h-[14px] w-px shrink-0 bg-divider-regular" />
Expand All @@ -533,7 +533,7 @@ const AppCard = ({ app, onlineUsers = [], onRefresh, onOpenTagManagement = () =>
aria-label={t('operation.more', { ns: 'common' })}
className={cn(
isOperationsMenuOpen ? 'bg-state-base-hover shadow-none' : 'bg-transparent',
'flex h-8 w-8 items-center justify-center rounded-md border-none p-2 hover:bg-state-base-hover',
'flex h-8 w-8 items-center justify-center rounded-md border-none p-2 hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:ring-inset',
)}
onClick={(e) => {
e.stopPropagation()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ describe('tool/tool-form/item', () => {
} as unknown as SchemaRoot,
})

const { container } = render(
render(
<ToolFormItem
readOnly={false}
nodeId="tool-node"
Expand All @@ -182,7 +182,8 @@ describe('tool/tool-form/item', () => {
/>,
)

fireEvent.mouseEnter(container.querySelector('svg')?.parentElement as HTMLElement)
const infotipTrigger = screen.getByRole('button', { name: 'Select from tools' })
fireEvent.click(infotipTrigger)
expect(screen.getByText('Select from tools'))!.toBeInTheDocument()

fireEvent.click(screen.getByRole('button', { name: 'JSON Schema' }))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
RiBracesLine,
} from '@remixicon/react'
import { useBoolean } from 'ahooks'
import Tooltip from '@/app/components/base/tooltip'
import { Infotip } from '@/app/components/base/infotip'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { SchemaModal } from '@/app/components/plugins/plugin-detail-panel/tool-selector/components'
Expand Down Expand Up @@ -100,15 +100,13 @@ const ToolFormItem: FC<Props> = ({
<div className="ml-1 system-xs-regular text-text-destructive-secondary">*</div>
)}
{!showDescription && tooltip && (
<Tooltip
popupContent={(
<div className="w-[200px]">
{tooltip[language] || tooltip.en_US}
</div>
)}
triggerClassName="ml-1 w-4 h-4"
asChild={false}
/>
<Infotip
aria-label={tooltip[language] || tooltip.en_US}
className="ml-1"
popupClassName="w-[200px]"
>
{tooltip[language] || tooltip.en_US}
</Infotip>
)}
{showSchemaButton && (
<>
Expand Down
27 changes: 16 additions & 11 deletions web/features/tag-management/__tests__/dataset-card-tags.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@ import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { DatasetCardTags } from '../components/dataset-card-tags'

// Mock TagSelector as it's a complex component from base
vi.mock('@/features/tag-management/components/tag-selector', () => ({
TagSelector: ({ selectedTagIds, selectedTags, onOpenTagManagement }: {
selectedTagIds: string[]
selectedTags: Tag[]
TagSelector: ({ value, onOpenTagManagement }: {
value: Tag[]
onOpenTagManagement?: () => void
}) => (
<div data-testid="tag-selector">
<div data-testid="tag-values">{selectedTagIds.join(',')}</div>
<div data-testid="tag-values">{value.map(tag => tag.id).join(',')}</div>
<div data-testid="selected-count">
{selectedTags.length}
{value.length}
{' '}
tags
</div>
Expand Down Expand Up @@ -75,7 +73,9 @@ describe('DatasetCardTags', () => {
const onClick = vi.fn()
const { container } = render(<DatasetCardTags {...defaultProps} onClick={onClick} />)

const wrapper = container.firstChild as HTMLElement
const wrapper = container.firstElementChild
if (!wrapper)
throw new Error('Expected dataset card tag wrapper')
fireEvent.click(wrapper)

expect(onClick).toHaveBeenCalledTimes(1)
Expand All @@ -94,13 +94,17 @@ describe('DatasetCardTags', () => {
describe('Styles', () => {
it('should have opacity class when embedding is not available', () => {
const { container } = render(<DatasetCardTags {...defaultProps} embeddingAvailable={false} />)
const wrapper = container.firstChild as HTMLElement
const wrapper = container.firstElementChild
if (!wrapper)
throw new Error('Expected dataset card tag wrapper')
expect(wrapper).toHaveClass('opacity-30')
})

it('should not have opacity class when embedding is available', () => {
const { container } = render(<DatasetCardTags {...defaultProps} embeddingAvailable={true} />)
const wrapper = container.firstChild as HTMLElement
const wrapper = container.firstElementChild
if (!wrapper)
throw new Error('Expected dataset card tag wrapper')
expect(wrapper).not.toHaveClass('opacity-30')
})

Expand All @@ -109,6 +113,7 @@ describe('DatasetCardTags', () => {
const maskDiv = container.querySelector('.bg-tag-selector-mask-bg')
expect(maskDiv).toBeInTheDocument()
expect(maskDiv).toHaveClass('group-hover/tag-area:hidden')
expect(maskDiv).toHaveClass('group-focus-within/tag-area:hidden')
expect(maskDiv).toHaveClass('group-hover:bg-tag-selector-mask-hover-bg')
})

Expand Down Expand Up @@ -139,10 +144,10 @@ describe('DatasetCardTags', () => {
})

it('should handle many tags', () => {
const manyTags: Tag[] = Array.from({ length: 20 }, (_, i) => ({
const manyTags: Tag[] = Array.from({ length: 20 }, (_, i): Tag => ({
id: `tag-${i}`,
name: `Tag ${i}`,
type: 'knowledge' as const,
type: 'knowledge',
binding_count: 0,
}))
render(<DatasetCardTags {...defaultProps} tags={manyTags} />)
Expand Down
16 changes: 8 additions & 8 deletions web/features/tag-management/__tests__/tag-filter.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ const defaultProps = {
// Helper: the i18n mock renders "ns.key" format (dot-separated)
const i18n = {
placeholder: 'common.tag.placeholder',
selectorPlaceholder: 'common.tag.selectorPlaceholder',
operationClear: 'common.operation.clear',
noTag: 'common.tag.noTag',
manageTags: 'common.tag.manageTags',
}
Expand Down Expand Up @@ -158,11 +160,9 @@ describe('TagFilter', () => {
await user.click(screen.getByText('Frontend'))

// The Check icon should be rendered for the selected tag
const tagItem = screen.getByTitle('Frontend')
const tagItem = screen.getByRole('option', { name: /Frontend/i })
expect(tagItem).toBeInTheDocument()
// The parent container of the tag has a Check SVG sibling
const checkIcons = screen.getAllByTestId('tag-filter-selected-icon')
expect(checkIcons?.length).toBeGreaterThanOrEqual(1)
expect(tagItem).toHaveAttribute('aria-selected', 'true')
})

it('should clear all selected tags when clear button is clicked', async () => {
Expand Down Expand Up @@ -197,7 +197,7 @@ describe('TagFilter', () => {

await user.click(screen.getByText(i18n.placeholder))

const searchInput = screen.getByRole('textbox')
const searchInput = screen.getByRole('combobox', { name: i18n.selectorPlaceholder })
await user.type(searchInput, 'Front')

expect(screen.getByText('Frontend')).toBeInTheDocument()
Expand All @@ -212,7 +212,7 @@ describe('TagFilter', () => {

await user.click(screen.getByText(i18n.placeholder))

const searchInput = screen.getByRole('textbox')
const searchInput = screen.getByRole('combobox', { name: i18n.selectorPlaceholder })
await user.type(searchInput, 'NonExistentTag')

expect(screen.getByText(i18n.noTag)).toBeInTheDocument()
Expand All @@ -225,12 +225,12 @@ describe('TagFilter', () => {

await user.click(screen.getByText(i18n.placeholder))

const searchInput = screen.getByRole('textbox')
const searchInput = screen.getByRole('combobox', { name: i18n.selectorPlaceholder })
await user.type(searchInput, 'Front')

expect(screen.queryByText('Backend')).not.toBeInTheDocument()

const clearButton = screen.getByTestId('input-clear')
const clearButton = screen.getByRole('button', { name: i18n.operationClear })
await user.click(clearButton)

expect(searchInput).toHaveValue('')
Expand Down
Loading
Loading