import React from 'react'; import { userEvent } from '@testing-library/user-event'; import { RouteComponentProps } from 'react-router-dom'; import { type MetadataTemplate, type MetadataTemplateInstance } from '@box/metadata-editor'; import { FIELD_PERMISSIONS_CAN_UPLOAD, ERROR_CODE_METADATA_STRUCTURED_TEXT_REP } from '../../../constants'; import { screen, render, waitFor } from '../../../test-utils/testing-library'; import { MetadataSidebarRedesignComponent as MetadataSidebarRedesign, type MetadataSidebarRedesignProps, } from '../MetadataSidebarRedesign'; import useSidebarMetadataFetcher, { STATUS } from '../hooks/useSidebarMetadataFetcher'; jest.mock('../hooks/useSidebarMetadataFetcher'); const mockUseSidebarMetadataFetcher = useSidebarMetadataFetcher as jest.MockedFunction< typeof useSidebarMetadataFetcher >; const getStructuredTextRep = jest.fn().mockResolvedValue('structured-text-rep'); const api = { options: { token: jest.fn().mockResolvedValue({ read: 'read-token-value', write: 'write-token-value', }), }, }; describe('elements/content-sidebar/Metadata/MetadataSidebarRedesign', () => { const mockTemplates: MetadataTemplate[] = [ { id: 'metadata_template_custom_1', scope: 'global', templateKey: 'properties', hidden: false, fields: [], type: 'metadata_template', }, ]; const mockTemplateInstance = { canEdit: true, id: '123', scope: 'global', templateKey: 'metadata_template_123', hidden: false, fields: [], type: 'metadata_template', } satisfies MetadataTemplateInstance; const mockCustomTemplateInstance = { canEdit: true, hidden: false, id: 'metadata_template_42', fields: [ { key: 'Another testing key', type: 'string', value: '42', hidden: false, }, { key: 'Test key', type: 'string', value: 'Some test value', hidden: false, }, ], scope: 'global', templateKey: 'properties', type: 'properties', } satisfies MetadataTemplateInstance; const mockVisibleTemplateInstance = { displayName: 'Visible Template', canEdit: true, hidden: false, fields: [], id: 'visible_template', scope: 'global', templateKey: 'visibleTemplate', type: 'metadata_template', } satisfies MetadataTemplateInstance; const mockHiddenTemplateInstance = { displayName: 'Hidden Template', canEdit: true, hidden: true, fields: [], id: 'hidden_template', scope: 'global', templateKey: 'hiddenTemplate', type: 'metadata_template', } satisfies MetadataTemplateInstance; const mockFile = { id: '123', permissions: { [FIELD_PERMISSIONS_CAN_UPLOAD]: true }, }; const renderComponent = (props = {}, features = {}) => { const emptyFilteredTemplateIds = []; const routeComponentProps = {} as RouteComponentProps; const defaultProps = { api, fileExtension: 'pdf', getStructuredTextRep, fileId: 'test-file-id-1', elementId: 'element-1', filteredTemplateIds: emptyFilteredTemplateIds, isFeatureEnabled: true, onError: jest.fn(), onSuccess: jest.fn(), ...routeComponentProps, } satisfies MetadataSidebarRedesignProps; render(, { wrapperProps: { features } }); }; beforeEach(() => { mockUseSidebarMetadataFetcher.mockReturnValue({ clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), handleUpdateMetadataInstance: jest.fn(), templates: mockTemplates, templateInstances: [], errorMessage: null, status: STATUS.SUCCESS, file: mockFile, extractErrorCode: null, }); }); afterEach(() => { jest.clearAllMocks(); }); test('should render title', () => { renderComponent(); expect(screen.getByRole('heading', { level: 2, name: 'Metadata' })).toBeInTheDocument(); }); test('should have accessible "All templates" combobox trigger button', () => { mockUseSidebarMetadataFetcher.mockReturnValue({ clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), handleUpdateMetadataInstance: jest.fn(), templateInstances: [mockTemplateInstance, mockCustomTemplateInstance], templates: mockTemplates, errorMessage: null, status: STATUS.SUCCESS, file: mockFile, extractErrorCode: null, }); renderComponent(); expect( screen.getAllByRole('combobox').find(combobox => combobox.textContent === 'All Templates'), ).toBeInTheDocument(); }); test('should have accessible "Add template" button', () => { renderComponent(); expect(screen.getByRole('button', { name: 'Add template' })).toBeInTheDocument(); }); test('should have selectable "Custom Metadata" template in dropdown', async () => { renderComponent(); const addTemplateButton = screen.getByRole('button', { name: 'Add template' }); await userEvent.click(addTemplateButton); const customMetadataOption = screen.getByRole('option', { name: 'Custom Metadata' }); expect(customMetadataOption).toBeInTheDocument(); userEvent.click(customMetadataOption); // instead of below assertions check if template was added when MetadataInstanceList will be implemented await userEvent.click(addTemplateButton); expect(customMetadataOption).toHaveAttribute('aria-disabled', 'true'); }); test('should have accessible "All templates" combobox trigger button', () => { mockUseSidebarMetadataFetcher.mockReturnValue({ clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), handleUpdateMetadataInstance: jest.fn(), templateInstances: [mockTemplateInstance, mockCustomTemplateInstance], templates: mockTemplates, errorMessage: null, status: STATUS.SUCCESS, file: mockFile, extractErrorCode: null, }); renderComponent(); expect( screen.getAllByRole('combobox').find(combobox => combobox.textContent === 'All Templates'), ).toBeInTheDocument(); }); test('should render metadata sidebar with error', async () => { mockUseSidebarMetadataFetcher.mockReturnValue({ clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), handleUpdateMetadataInstance: jest.fn(), templateInstances: [], templates: [], errorMessage: { id: 'error', defaultMessage: 'error message', }, status: STATUS.ERROR, file: mockFile, extractErrorCode: null, }); const errorMessage = { id: 'error', defaultMessage: 'error message' }; renderComponent(); expect(screen.getByRole('heading', { level: 2, name: 'Metadata' })).toBeInTheDocument(); expect(screen.getByText(errorMessage.defaultMessage)).toBeInTheDocument(); }); test('should render metadata sidebar with loading indicator', async () => { mockUseSidebarMetadataFetcher.mockReturnValue({ clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), handleUpdateMetadataInstance: jest.fn(), templateInstances: [], templates: [], errorMessage: null, status: STATUS.LOADING, file: mockFile, extractErrorCode: null, }); renderComponent(); expect(screen.getByRole('heading', { level: 2, name: 'Metadata' })).toBeInTheDocument(); expect(screen.getByRole('status', { name: 'Loading' })).toBeInTheDocument(); expect(screen.getByRole('status', { name: 'Loading' })).toBeInTheDocument(); }); test('should correctly render empty state when AI feature is enabled', () => { renderComponent({}, { 'metadata.aiSuggestions.enabled': true }); expect(screen.getByRole('heading', { level: 2, name: 'Add Metadata Templates' })).toBeInTheDocument(); expect( screen.getByText('Add Metadata to your file to support business operations, workflows, and more!'), ).toBeInTheDocument(); }); test('should correctly render empty state when AI feature is disabled', () => { renderComponent( { isBoxAiSuggestionsEnabled: false }, { wrapperProps: { features: { 'metadata.aiSuggestions.enabled': false } } }, ); expect(screen.getByRole('heading', { level: 2, name: 'Add Metadata Templates' })).toBeInTheDocument(); expect( screen.getByText('Add Metadata to your file to support business operations, workflows, and more!'), ).toBeInTheDocument(); }); test('should render empty state when no visible template instances are present', () => { mockUseSidebarMetadataFetcher.mockReturnValue({ clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), handleUpdateMetadataInstance: jest.fn(), templateInstances: [mockHiddenTemplateInstance], templates: mockTemplates, errorMessage: null, status: STATUS.SUCCESS, file: mockFile, extractErrorCode: null, }); renderComponent(); expect(screen.getByRole('heading', { level: 2, name: 'Add Metadata Templates' })).toBeInTheDocument(); expect( screen.getByText('Add Metadata to your file to support business operations, workflows, and more!'), ).toBeInTheDocument(); expect(screen.queryByRole('heading', { level: 4, name: 'Hidden Template' })).not.toBeInTheDocument(); }); test('should render metadata instance list when templates are present', () => { mockUseSidebarMetadataFetcher.mockReturnValue({ clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), handleUpdateMetadataInstance: jest.fn(), templateInstances: [mockCustomTemplateInstance, mockVisibleTemplateInstance], templates: mockTemplates, errorMessage: null, status: STATUS.SUCCESS, file: mockFile, extractErrorCode: null, }); renderComponent(); expect(screen.getByRole('heading', { level: 2, name: 'Metadata' })).toBeInTheDocument(); expect(screen.getByRole('heading', { level: 4, name: 'Custom Metadata' })).toBeInTheDocument(); expect(screen.getByText(mockCustomTemplateInstance.fields[0].key)).toBeInTheDocument(); expect(screen.getByText(mockCustomTemplateInstance.fields[1].key)).toBeInTheDocument(); expect(screen.getByRole('heading', { level: 4, name: 'Visible Template' })).toBeInTheDocument(); }); test('should render filter dropdown when more than one templates are present', () => { mockUseSidebarMetadataFetcher.mockReturnValue({ clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), handleUpdateMetadataInstance: jest.fn(), templateInstances: [mockCustomTemplateInstance, mockVisibleTemplateInstance], templates: mockTemplates, errorMessage: null, status: STATUS.SUCCESS, file: mockFile, extractErrorCode: null, }); renderComponent(); const filterDropdown = screen.getByRole('combobox'); expect(filterDropdown.textContent).toContain('All Templates'); }); test.each([ [[mockVisibleTemplateInstance, mockHiddenTemplateInstance]], // One visible and one hidden template [[mockVisibleTemplateInstance]], // One visible template [[]], // No templates [[mockHiddenTemplateInstance]], // One hidden template ])( 'should not render filter dropdown when only one or none visible template is present', (templateInstances: MetadataTemplateInstance[]) => { mockUseSidebarMetadataFetcher.mockReturnValue({ clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), handleUpdateMetadataInstance: jest.fn(), templateInstances, templates: mockTemplates, errorMessage: null, status: STATUS.SUCCESS, file: mockFile, extractErrorCode: null, }); renderComponent(); expect(screen.queryByRole('combobox')).not.toBeInTheDocument(); }, ); test('should render metadata filterd instance list when fileterd templates are present and matching', () => { mockUseSidebarMetadataFetcher.mockReturnValue({ clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), handleUpdateMetadataInstance: jest.fn(), templateInstances: [mockCustomTemplateInstance, mockVisibleTemplateInstance], templates: mockTemplates, errorMessage: null, status: STATUS.SUCCESS, file: mockFile, extractErrorCode: null, }); const filteredTemplateIds = [mockVisibleTemplateInstance.id]; renderComponent({ filteredTemplateIds }); expect(screen.getByRole('heading', { level: 2, name: 'Metadata' })).toBeInTheDocument(); expect(screen.getByRole('heading', { level: 4, name: 'Visible Template' })).toBeInTheDocument(); expect(screen.queryByRole('heading', { level: 4, name: 'Custom Metadata' })).not.toBeInTheDocument(); }); test('should render metadata unfiltered instance list when fileterd templates are present and do not match existing templates', () => { mockUseSidebarMetadataFetcher.mockReturnValue({ clearExtractError: jest.fn(), extractSuggestions: jest.fn(), handleCreateMetadataInstance: jest.fn(), handleDeleteMetadataInstance: jest.fn(), handleUpdateMetadataInstance: jest.fn(), templateInstances: [mockCustomTemplateInstance, mockVisibleTemplateInstance], templates: mockTemplates, errorMessage: null, status: STATUS.SUCCESS, file: mockFile, extractErrorCode: null, }); const filteredTemplateIds = ['non-existing-template-id']; renderComponent({ filteredTemplateIds }); expect(screen.getByRole('heading', { level: 2, name: 'Metadata' })).toBeInTheDocument(); expect(screen.getByRole('heading', { level: 4, name: 'Custom Metadata' })).toBeInTheDocument(); expect(screen.getByText(mockCustomTemplateInstance.fields[0].key)).toBeInTheDocument(); expect(screen.getByText(mockCustomTemplateInstance.fields[1].key)).toBeInTheDocument(); expect(screen.getByRole('heading', { level: 4, name: 'Visible Template' })).toBeInTheDocument(); }); test('should call getStructuredTextRep', async () => { renderComponent({ api }, { 'metadata.aiSuggestions.enabled': true }); await waitFor(() => expect(getStructuredTextRep).toHaveBeenCalledTimes(1)); }); test.each` description | isFeatureEnabled | fileExtension ${'isBoxAiSuggestionsEnabled is false'} | ${false} | ${'pdf'} ${'fileExtension is not a pdf'} | ${true} | ${'docx'} ${'isBoxAiSuggestionsEnabled is false and fileExtension is not a pdf'} | ${false} | ${'docx'} `('should not call getStructuredTextRep when $description', async ({ isFeatureEnabled, fileExtension }) => { renderComponent({ api, fileExtension }, { 'metadata.aiSuggestions.enabled': isFeatureEnabled }); await waitFor(() => expect(getStructuredTextRep).not.toHaveBeenCalled()); }); test('should call onError when getStructuredTextRep fails', async () => { const getStructuredTextRepError = jest.fn().mockRejectedValue(new Error('Failed to fetch structured text')); const onError = jest.fn(); renderComponent( { getStructuredTextRep: getStructuredTextRepError, api, onError }, { 'metadata.aiSuggestions.enabled': true }, ); await waitFor(() => { expect(onError).toHaveBeenCalledWith(expect.any(Error), ERROR_CODE_METADATA_STRUCTURED_TEXT_REP); }); }); test('should call createSessionRequest once', async () => { const createSessionRequest = jest.fn().mockResolvedValue({}); renderComponent({ api, createSessionRequest }, { 'metadata.aiSuggestions.enabled': true }); expect(createSessionRequest).toHaveBeenCalledTimes(1); expect(createSessionRequest).toHaveBeenCalledWith({ items: [{ id: 'test-file-id-1' }] }, 'test-file-id-1'); }); test('should not call createSessionRequest once when fileId is not provided', async () => { const createSessionRequest = jest.fn().mockResolvedValue({}); renderComponent({ api, createSessionRequest, fileId: undefined }, { 'metadata.aiSuggestions.enabled': true }); expect(createSessionRequest).not.toHaveBeenCalledTimes(1); expect(createSessionRequest).not.toHaveBeenCalledWith({ items: [{ id: undefined }] }, undefined); }); });