import * as React from 'react'; import type { Collection } from '../../../common/types/core'; import type { MetadataTemplate, MetadataTemplateField } from '../../../common/types/metadata'; import { render, screen, userEvent, waitFor, within } from '../../../test-utils/testing-library'; import MetadataViewContainer, { type MetadataViewContainerProps, convertFilterValuesToExternal, type ExternalFilterValues, } from '../MetadataViewContainer'; Object.defineProperty(Element.prototype, 'scrollTo', { value: jest.fn(), writable: true, }); describe('elements/content-explorer/MetadataViewContainer', () => { const mockItems = [ { id: '1', name: 'File 1.txt', type: 'file', 'item.name': 'File 1.txt', industry: 'tech', }, { id: '2', name: 'File 2.pdf', type: 'file', 'item.name': 'File 2.pdf', industry: 'finance', }, ]; const mockMetadataTemplateFields: MetadataTemplateField[] = [ { id: 'field2', key: 'industry', displayName: 'Industry', type: 'enum', options: [ { key: 'tech', id: 'tech1' }, { key: 'finance', id: 'finance1' }, ], }, { id: 'field3', key: 'price', displayName: 'Price', type: 'float', }, { id: 'field4', key: 'category', displayName: 'Category', type: 'multiSelect', options: [ { key: 'category1', id: 'cat1' }, { key: 'category2', id: 'cat2' }, ], }, ]; const mockMetadataTemplate: MetadataTemplate = { id: 'template1', scope: 'enterprise', templateKey: 'testTemplate', displayName: 'Test Template', fields: mockMetadataTemplateFields, }; const mockCollection: Collection = { id: '0', items: mockItems, percentLoaded: 100, }; const defaultProps: MetadataViewContainerProps = { currentCollection: mockCollection, columns: [ { textValue: 'Name', id: 'item.name', type: 'string', allowsSorting: true, minWidth: 250, maxWidth: 250, isRowHeader: true, }, { textValue: 'Industry', id: 'industry', type: 'string', allowsSorting: true, minWidth: 250, maxWidth: 250, }, { textValue: 'Contact Role', id: 'role', type: 'string', allowsSorting: true, minWidth: 250, maxWidth: 250, }, { textValue: 'Status', id: 'status', type: 'string', allowsSorting: true, minWidth: 250, maxWidth: 250, }, { textValue: 'Price', id: 'price', type: 'string', allowsSorting: true, minWidth: 250, maxWidth: 250, }, ], metadataTemplate: mockMetadataTemplate, onMetadataFilter: jest.fn(), }; const renderComponent = (props: Partial = {}) => { return render(); }; let user; beforeEach(() => { user = userEvent(); jest.clearAllMocks(); }); test('should render MetadataView component', () => { renderComponent(); expect(screen.getByRole('button', { name: 'All Filters' })).toBeInTheDocument(); expect(screen.getAllByRole('button', { name: 'Name' })).toHaveLength(2); // One in filter bar, one in table header expect(screen.getByRole('button', { name: 'Industry' })).toBeInTheDocument(); expect(screen.getByText('File 1.txt')).toBeInTheDocument(); expect(screen.getByText('File 2.pdf')).toBeInTheDocument(); }); test('should pass values as string[] on submit', async () => { const onFilterSubmit = jest.fn(); const template: MetadataTemplate = { ...mockMetadataTemplate, fields: [ { id: 'ms1', key: 'role', displayName: 'Contact Role', type: 'multiSelect', options: [ { id: 'r1', key: 'Developer' }, { id: 'r2', key: 'Marketing' }, { id: 'r3', key: 'Sales' }, ], }, ], }; renderComponent({ metadataTemplate: template, actionBarProps: { onFilterSubmit }, onMetadataFilter: jest.fn(), }); await user.click(screen.getByRole('button', { name: /Contact Role/ })); await user.click(within(screen.getByRole('menu')).getByRole('menuitemcheckbox', { name: 'Developer' })); // Re-open the chip to select a second value (menu closes after submit) await user.click(screen.getByRole('button', { name: /Contact Role/ })); await user.click(within(screen.getByRole('menu')).getByRole('menuitemcheckbox', { name: 'Marketing' })); await waitFor(() => expect(onFilterSubmit).toHaveBeenCalledTimes(2)); const firstCall = onFilterSubmit.mock.calls[0][0]; const secondCall = onFilterSubmit.mock.calls[1][0]; expect(firstCall.role.value).toEqual(['Developer']); expect(secondCall.role.value).toEqual(['Developer', 'Marketing']); }); test('should call onMetadataFilter and onFilterSubmit when filter is submitted', async () => { const onFilterSubmit = jest.fn(); const onMetadataFilter = jest.fn(); const template: MetadataTemplate = { ...mockMetadataTemplate, fields: [ { id: 'field1', key: 'status', displayName: 'Status', type: 'enum', options: [ { id: 's1', key: 'Active' }, { id: 's2', key: 'Inactive' }, ], }, ], }; renderComponent({ metadataTemplate: template, actionBarProps: { onFilterSubmit }, onMetadataFilter, }); await user.click(screen.getByRole('button', { name: /Status/ })); await user.click(within(screen.getByRole('menu')).getByRole('menuitemcheckbox', { name: 'Active' })); await waitFor(() => { expect(onMetadataFilter).toHaveBeenCalledTimes(1); expect(onFilterSubmit).toHaveBeenCalledTimes(1); }); const filterCall = onMetadataFilter.mock.calls[0][0]; const submitCall = onFilterSubmit.mock.calls[0][0]; expect(filterCall.status.value).toEqual(['Active']); expect(submitCall.status.value).toEqual(['Active']); }); test('should only call onMetadataFilter when onFilterSubmit is not provided', async () => { const onMetadataFilter = jest.fn(); const template: MetadataTemplate = { ...mockMetadataTemplate, fields: [ { id: 'field1', key: 'status', displayName: 'Status', type: 'enum', options: [ { id: 's1', key: 'Active' }, { id: 's2', key: 'Inactive' }, ], }, ], }; renderComponent({ metadataTemplate: template, onMetadataFilter, }); await user.click(screen.getByRole('button', { name: /Status/ })); await user.click(within(screen.getByRole('menu')).getByRole('menuitemcheckbox', { name: 'Active' })); await waitFor(() => { expect(onMetadataFilter).toHaveBeenCalledTimes(1); }); const filterCall = onMetadataFilter.mock.calls[0][0]; expect(filterCall.status.value).toEqual(['Active']); }); test('should handle initial filter values transformation', () => { const initialFilterValues = { industry: { fieldType: 'enum' as const, value: ['tech'], }, price: { fieldType: 'float' as const, value: { range: { gt: 10, lt: 100 } }, }, name: { fieldType: 'string' as const, value: ['search term'], }, category: { fieldType: 'multiSelect' as const, value: ['category1', 'category2'], }, } as unknown as ExternalFilterValues; renderComponent({ actionBarProps: { initialFilterValues }, }); expect(screen.getByRole('button', { name: 'All Filters 2' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Industry/i })).toHaveTextContent(/\(1\)/); // Category filter should not be present since there's no corresponding column expect(screen.queryByRole('button', { name: /Category/i })).not.toBeInTheDocument(); }); test('should handle empty metadata template fields', () => { const emptyTemplate: MetadataTemplate = { ...mockMetadataTemplate, fields: [], }; renderComponent({ metadataTemplate: emptyTemplate }); expect(screen.getByRole('button', { name: 'All Filters' })).toBeInTheDocument(); expect(screen.getByText('File 1.txt')).toBeInTheDocument(); expect(screen.getByText('File 2.pdf')).toBeInTheDocument(); }); test('should handle undefined metadata template', () => { renderComponent({ metadataTemplate: undefined as unknown as MetadataTemplate }); expect(screen.getByRole('button', { name: 'All Filters' })).toBeInTheDocument(); expect(screen.getByText('File 1.txt')).toBeInTheDocument(); expect(screen.getByText('File 2.pdf')).toBeInTheDocument(); }); test('should handle empty collection items', () => { const emptyCollection: Collection = { id: '0', items: [], percentLoaded: 100, }; renderComponent({ currentCollection: emptyCollection }); expect(screen.getByRole('button', { name: 'All Filters' })).toBeInTheDocument(); }); test('should handle undefined collection items', () => { const collectionWithoutItems: Collection = { id: '0', percentLoaded: 100, }; renderComponent({ currentCollection: collectionWithoutItems }); expect(screen.getByRole('button', { name: 'All Filters' })).toBeInTheDocument(); }); test('should memoize filterGroups when metadataTemplate changes', () => { const { rerender } = renderComponent(); // Re-render with same template rerender(); // Re-render with different template const newTemplate: MetadataTemplate = { ...mockMetadataTemplate, id: 'template2', displayName: 'New Template', }; rerender(); expect(screen.getByRole('button', { name: 'All Filters' })).toBeInTheDocument(); }); test('should handle fields with no options', () => { const templateWithoutOptions: MetadataTemplate = { ...mockMetadataTemplate, fields: [ { id: 'field2', key: 'industry', displayName: 'Industry', type: 'enum', // No options defined }, ], }; renderComponent({ metadataTemplate: templateWithoutOptions, }); expect(screen.getByRole('button', { name: 'All Filters' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Industry' })).toBeInTheDocument(); }); test('should handle multiple field types in filter submission', async () => { const onFilterSubmit = jest.fn(); const onMetadataFilter = jest.fn(); const template: MetadataTemplate = { ...mockMetadataTemplate, fields: [ { id: 'field1', key: 'status', displayName: 'Status', type: 'enum', options: [ { id: 's1', key: 'Active' }, { id: 's2', key: 'Inactive' }, ], }, { id: 'field2', key: 'price', displayName: 'Price', type: 'float', }, ], }; renderComponent({ metadataTemplate: template, actionBarProps: { onFilterSubmit }, onMetadataFilter, }); // Test enum filter await user.click(screen.getByRole('button', { name: /Status/ })); await user.click(within(screen.getByRole('menu')).getByRole('menuitemcheckbox', { name: 'Active' })); await waitFor(() => { expect(onMetadataFilter).toHaveBeenCalledTimes(1); expect(onFilterSubmit).toHaveBeenCalledTimes(1); }); const filterCall = onMetadataFilter.mock.calls[0][0]; expect(filterCall.status.value).toEqual(['Active']); expect(filterCall.status.fieldType).toBe('enum'); }); describe('convertFilterValuesToExternal', () => { test('should convert enum values to string arrays', () => { const internalFilters = { 'status-filter': { fieldType: 'enum' as const, options: [ { key: 'active', id: 'active1' }, { key: 'inactive', id: 'inactive1' }, ], value: { enum: ['active', 'inactive'] }, }, }; const result = convertFilterValuesToExternal(internalFilters); expect(result['status-filter'].value).toEqual(['active', 'inactive']); expect(result['status-filter'].fieldType).toBe('enum'); expect(result['status-filter'].options).toEqual([ { key: 'active', id: 'active1' }, { key: 'inactive', id: 'inactive1' }, ]); }); test('should keep range values unchanged', () => { const internalFilters = { 'price-filter': { fieldType: 'float' as const, value: { range: { gt: 10, lt: 100 }, advancedFilterOption: 'range' }, }, }; const result = convertFilterValuesToExternal(internalFilters); expect(result['price-filter'].value).toEqual({ range: { gt: 10, lt: 100 }, advancedFilterOption: 'range' }); expect(result['price-filter'].fieldType).toBe('float'); }); test('should keep float values unchanged', () => { const internalFilters = { 'rating-filter': { fieldType: 'float' as const, value: { range: { gt: 4.5, lt: 5.0 }, advancedFilterOption: 'range' }, }, }; const result = convertFilterValuesToExternal(internalFilters); expect(result['rating-filter'].value).toEqual({ range: { gt: 4.5, lt: 5.0 }, advancedFilterOption: 'range', }); expect(result['rating-filter'].fieldType).toBe('float'); }); test('should handle mixed field types', () => { const internalFilters = { 'status-filter': { fieldType: 'enum' as const, options: [ { key: 'active', id: 'active1' }, { key: 'inactive', id: 'inactive1' }, ], value: { enum: ['active'] }, }, 'price-filter': { fieldType: 'float' as const, value: { range: { gt: 0, lt: 50 }, advancedFilterOption: 'range' }, }, 'category-filter': { fieldType: 'multiSelect' as const, options: [ { key: 'tech', id: 'tech1' }, { key: 'finance', id: 'finance1' }, { key: 'healthcare', id: 'healthcare1' }, ], value: { enum: ['tech', 'finance'] }, }, }; const result = convertFilterValuesToExternal(internalFilters); expect(result['status-filter'].value).toEqual(['active']); expect(result['price-filter'].value).toEqual({ range: { gt: 0, lt: 50 }, advancedFilterOption: 'range' }); expect(result['category-filter'].value).toEqual(['tech', 'finance']); }); test('should handle empty filter object', () => { const result = convertFilterValuesToExternal({}); expect(result).toEqual({}); }); test('should handle enum values with empty array', () => { const internalFilters = { 'status-filter': { fieldType: 'enum' as const, options: [{ key: 'active', id: 'active1' }], value: { enum: [] }, }, }; const result = convertFilterValuesToExternal(internalFilters); expect(result['status-filter'].value).toEqual([]); expect(result['status-filter'].fieldType).toBe('enum'); }); test('should handle multiSelect values', () => { const internalFilters = { 'category-filter': { fieldType: 'multiSelect' as const, options: [ { key: 'tech', id: 'tech1' }, { key: 'finance', id: 'finance1' }, ], value: { enum: ['tech', 'finance'] }, }, }; const result = convertFilterValuesToExternal(internalFilters); expect(result['category-filter'].value).toEqual(['tech', 'finance']); expect(result['category-filter'].fieldType).toBe('multiSelect'); }); }); describe('predefined filter options', () => { test('should only show metadata template filters in sidepanel', async () => { renderComponent(); expect(screen.getByRole('button', { name: 'All Filters' })).toBeInTheDocument(); await user.click(screen.getByRole('button', { name: 'All Filters' })); expect(screen.getByRole('dialog')).toBeInTheDocument(); expect(within(screen.getByRole('dialog')).getByRole('button', { name: 'File Type' })).toBeInTheDocument(); // Should NOT show predefined filters that are disabled expect(within(screen.getByRole('dialog')).queryByPlaceholderText('Enter keywords')).not.toBeInTheDocument(); expect( within(screen.getByRole('dialog')).queryByRole('button', { name: /location/i }), ).not.toBeInTheDocument(); }); }); test('should filter fields based on columns provided', () => { const template: MetadataTemplate = { ...mockMetadataTemplate, fields: [ { id: 'field1', key: 'status', displayName: 'Status', type: 'enum', options: [ { id: 's1', key: 'Active' }, { id: 's2', key: 'Inactive' }, ], }, { id: 'field2', key: 'price', displayName: 'Price', type: 'float', }, { id: 'field3', key: 'category', displayName: 'Category', type: 'multiSelect', options: [ { id: 'c1', key: 'tech' }, { id: 'c2', key: 'finance' }, ], }, ], }; // Only provide columns for 'status' and 'price', not 'category' renderComponent({ metadataTemplate: template, }); // Should show filters for fields that have corresponding columns expect(screen.getByRole('button', { name: 'All Filters' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Status' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Price' })).toBeInTheDocument(); // Should NOT show filter for 'category' since there's no corresponding column expect(screen.queryByRole('button', { name: 'Category' })).not.toBeInTheDocument(); // Should NOT show filter for 'industry' since it's not in the provided columns expect(screen.queryByRole('button', { name: 'Industry' })).not.toBeInTheDocument(); }); });