import React from 'react';
import { userEvent } from '@testing-library/user-event';
import { render, screen } from '../../../test-utils/testing-library';
import BoxAISidebar, { BoxAISidebarProps } from '../BoxAISidebar';
let MockBoxAiAgentSelectorWithApi: jest.Mock;
let mockUseAgents: jest.Mock;
jest.mock('@box/box-ai-agent-selector', () => {
MockBoxAiAgentSelectorWithApi = jest.fn();
mockUseAgents = jest.fn();
return {
...jest.requireActual('@box/box-ai-agent-selector'),
BoxAiAgentSelectorWithApi: MockBoxAiAgentSelectorWithApi,
useAgents: mockUseAgents,
};
});
const mockOnClearAction = jest.fn();
jest.mock('@box/box-ai-content-answers', () => ({
...jest.requireActual('@box/box-ai-content-answers'),
// BoxAiContentAnswers: jest.fn().mockImplementation(() =>
),
withApiWrapper: Component => props => (
),
}));
jest.mock('../BoxAISidebarTitle', () => () => );
describe('elements/content-sidebar/BoxAISidebar', () => {
const mockAgents = {
agents: [],
requestState: 'success',
selectedAgent: null,
};
const mockProps = {
contentName: 'testName.txt',
cache: {
encodedSession: '',
questions: [],
agents: mockAgents,
shouldShowLandingPage: true,
suggestedQuestions: [],
},
createSessionRequest: jest.fn(() => ({ encodedSession: '1234' })),
elementId: '123',
fetchTimeout: {},
fileExtension: 'txt',
fileID: '123',
getAgentConfig: jest.fn(),
getAIStudioAgents: jest.fn(() => [
{
id: null,
name: 'Default agent',
isSelected: true,
},
{
id: 'special',
name: 'Special agent',
isSelected: false,
},
]),
getAnswer: jest.fn(),
getAnswerStreaming: jest.fn(),
getSuggestedQuestions: jest.fn(),
hostAppName: 'appName',
isLoading: false,
items: [
{
id: '1',
type: 'type',
fileType: 'pdf',
name: 'test.pdf',
status: 'supported',
},
],
itemSize: '1234',
isAgentSelectorEnabled: false,
isAIStudioAgentSelectorEnabled: true,
isCitationsEnabled: true,
isDebugModeEnabled: true,
isFeedbackEnabled: true,
isFeedbackFormEnabled: true,
isIntelligentQueryMode: true,
isMarkdownEnabled: true,
isStopResponseEnabled: true,
isStreamingEnabled: true,
onFeedbackFormSubmit: jest.fn(),
onUserInteraction: jest.fn(),
recordAction: jest.fn(),
sendQuestion: jest.fn(),
setCacheValue: jest.fn(),
shouldFeedbackFormIncludeFeedbackText: false,
shouldPreinitSession: true,
setHasQuestions: jest.fn(),
} as unknown as BoxAISidebarProps;
const renderComponent = async (props = {}) => {
await React.act(async () => {
render();
});
};
beforeAll(() => {
// Required to pass Blueprint Interactivity test for buttons with tooltip
Object.defineProperty(HTMLElement.prototype, 'offsetParent', {
get() {
return this.parentNode;
},
});
});
beforeEach(() => {
MockBoxAiAgentSelectorWithApi.mockImplementation(() => );
mockUseAgents.mockReturnValue({
agents: [],
selectedAgent: { id: '1', config: {}, name: 'Test Agent' },
setSelectedAgent: jest.fn(),
requestState: 'success',
});
});
afterEach(() => {
jest.clearAllMocks();
});
test('should render BoxAISidebarTitle', async () => {
await renderComponent();
expect(screen.queryByTestId('boxai-sidebar-title')).toBeInTheDocument();
});
test('should have accessible Agent selector if isAIStudioAgentSelectorEnabled is true', async () => {
await renderComponent();
expect(screen.queryByTestId('sidebar-agent-selector')).toBeInTheDocument();
});
test('should not have accessible Agent selector if isAIStudioAgentSelectorEnabled is false', async () => {
await renderComponent({ isAIStudioAgentSelectorEnabled: false });
expect(screen.queryByTestId('sidebar-agent-selector')).not.toBeInTheDocument();
});
test('should have accessible "Clear" button', async () => {
await renderComponent();
expect(screen.getByRole('button', { name: 'Clear conversation' })).toBeInTheDocument();
});
test('should call recordAction on load if provided', async () => {
const mockRecordAction = jest.fn();
await renderComponent({ recordAction: mockRecordAction });
expect(mockRecordAction).toHaveBeenCalledWith({
action: 'programmatic',
component: 'sidebar',
feature: 'answers',
target: 'loaded',
data: {
items: [{ fileType: 'pdf', status: 'supported' }],
},
});
});
test('should call setHasQuestions with "false" on load if questions are empty', async () => {
await renderComponent();
expect(mockProps.setHasQuestions).toHaveBeenCalledWith(false);
});
test('should call setHasQuestions with "true" on load if questions are not empty', async () => {
await renderComponent({
cache: {
encodedSession: '1234',
questions: [
{
error: 'general',
isCompleted: true,
prompt: 'completed question',
},
],
agents: mockAgents,
shouldShowLandingPage: false,
suggestedQuestions: [],
},
});
expect(mockProps.setHasQuestions).toHaveBeenCalledWith(true);
});
test('should call onClearClick when click "Clear" button', async () => {
await renderComponent();
const expandButton = screen.getByRole('button', { name: 'Clear conversation' });
await userEvent.click(expandButton);
expect(mockOnClearAction).toHaveBeenCalled();
});
test('should pre-initialize session', async () => {
await renderComponent();
expect(mockProps.createSessionRequest).toHaveBeenCalled();
});
test('should not pre-initialize session when shouldPreinitSession = false', async () => {
await renderComponent({ shouldPreinitSession: false });
expect(mockProps.createSessionRequest).not.toHaveBeenCalled();
});
test('should render welcome message', async () => {
await renderComponent();
expect(screen.getByText('Welcome to Box AI', { exact: false })).toBeInTheDocument();
});
test('should render cached custom suggested questions', async () => {
await renderComponent({
cache: {
encodedSession: '1234',
questions: [],
agents: mockAgents,
shouldShowLandingPage: true,
suggestedQuestions: [
{
id: 'suggested-question-1',
prompt: 'Summarize this document',
label: 'Please summarize this document',
},
],
},
getSuggestedQuestions: jest.fn(),
});
expect(screen.getByText('Summarize this document', { exact: false })).toBeInTheDocument();
expect(screen.queryByText('Loading suggested questions', { exact: false })).not.toBeInTheDocument();
});
test('should not set questions that are in progress', async () => {
await renderComponent({
cache: {
encodedSession: '1234',
questions: [
{
error: 'general',
isCompleted: true,
prompt: 'completed question',
},
{
error: 'general',
isCompleted: false,
prompt: 'not completed question',
},
],
agents: mockAgents,
shouldShowLandingPage: false,
suggestedQuestions: [],
},
});
expect(screen.getByText('completed question')).toBeInTheDocument();
expect(screen.queryByText('not completed question')).not.toBeInTheDocument();
});
test('should display clear conversation tooltip', async () => {
await renderComponent();
const button = screen.getByRole('button', { name: 'Clear conversation' });
await userEvent.hover(button);
const tooltip = await screen.findByRole('tooltip', { name: 'Clear conversation' });
expect(tooltip).toBeInTheDocument();
});
test('should have accessible "Switch to modal view" button', async () => {
await renderComponent();
expect(screen.getByRole('button', { name: 'Switch to modal view' })).toBeInTheDocument();
});
test('should display "Switch to modal view" tooltip', async () => {
await renderComponent();
const button = screen.getByRole('button', { name: 'Switch to modal view' });
await userEvent.hover(button);
const tooltip = await screen.findByRole('tooltip', { name: 'Switch to modal view' });
expect(tooltip).toBeInTheDocument();
});
test('should open Intelligence Modal when clicking on "Switch to modal view" button and close when clicking "Switch to sidebar view"', async () => {
await renderComponent();
const switchToModalButton = screen.getByRole('button', { name: 'Switch to modal view' });
await userEvent.click(switchToModalButton);
expect(await screen.findByTestId('content-answers-modal')).toBeInTheDocument();
const switchToSidebarButton = screen.getByRole('button', { name: 'Switch to sidebar view' });
await userEvent.click(switchToSidebarButton);
expect(screen.queryByTestId('content-answers-modal')).not.toBeInTheDocument();
});
describe('remote sidebar component', () => {
const MockRemoteSidebar = jest.fn(() => );
const renderRemoteModule = jest.fn(() => );
test('should render remote sidebar component when provided', async () => {
await renderComponent({ renderRemoteModule });
expect(renderRemoteModule).toHaveBeenCalledWith(mockProps.elementId);
expect(screen.getByTestId('remote-sidebar')).toBeInTheDocument();
});
test('should not render default sidebar when remote component is provided', async () => {
await renderComponent({ renderRemoteModule });
expect(screen.queryByTestId('boxai-sidebar-title')).not.toBeInTheDocument();
expect(screen.queryByTestId('sidebar-agent-selector')).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Clear conversation' })).not.toBeInTheDocument();
});
});
describe('given shouldPreinitSession = false, should create session on user intent', () => {
test('agents list open', async () => {
MockBoxAiAgentSelectorWithApi.mockImplementation(({ onAgentsListOpen }) => {
onAgentsListOpen();
return ;
});
await renderComponent({ shouldPreinitSession: false });
expect(mockProps.createSessionRequest).toHaveBeenCalled();
});
test('content answers user action', async () => {
await renderComponent({ shouldPreinitSession: false });
const input = screen.getByTestId('content-answers-question-input');
input.focus();
await userEvent.keyboard('foo');
expect(mockProps.createSessionRequest).toHaveBeenCalled();
});
});
describe('given shouldPreinitSession = false, should not create session on user intent', () => {
test.each`
encodedSession | isLoading
${'123'} | ${true}
${null} | ${true}
${'123'} | ${false}
`(
'agents list open when encodedSession = $encodedSession and isLoading = $isLoading',
async ({ encodedSession, isLoading }) => {
MockBoxAiAgentSelectorWithApi.mockImplementation(({ onAgentsListOpen }) => {
onAgentsListOpen();
return ;
});
await renderComponent({ restoredSession: encodedSession, isLoading, shouldPreinitSession: false });
expect(mockProps.createSessionRequest).not.toHaveBeenCalled();
},
);
test.each`
encodedSession | isLoading
${'123'} | ${true}
${null} | ${true}
${'123'} | ${false}
`(
'content answers user action when encodedSession = $encodedSession and isLoading = $isLoading',
async ({ encodedSession, isLoading }) => {
await renderComponent({
cache: { ...mockProps.cache, encodedSession },
isLoading,
shouldPreinitSession: false,
});
const input = screen.getByTestId('content-answers-question-input');
input.focus();
await userEvent.keyboard('foo');
expect(mockProps.createSessionRequest).not.toHaveBeenCalled();
},
);
});
test('given shouldPreinitSession = false, should send question when encodedSession is available and last question is loading', async () => {
const mockSendQuestion = jest.fn();
await renderComponent({
cache: {
...mockProps.cache,
encodedSession: '123',
questions: [
{
error: null,
isCompleted: false,
isLoading: true,
prompt: 'foo',
},
],
},
sendQuestion: mockSendQuestion,
shouldPreinitSession: false,
});
expect(mockSendQuestion).toHaveBeenCalled();
});
test.each`
encodedSession | isLoading
${'123'} | ${false}
${null} | ${true}
${null} | ${false}
`(
'given shouldPreinitSession = false, should not send question when encodedSession = $encodedSession and last question loading state = $isLoading',
async ({ encodedSession, isLoading }) => {
const mockSendQuestion = jest.fn();
await renderComponent({
cache: {
...mockProps.cache,
encodedSession,
questions: [
{
error: null,
isCompleted: !isLoading,
isLoading,
prompt: 'foo',
},
],
},
sendQuestion: mockSendQuestion,
shouldPreinitSession: false,
});
expect(mockSendQuestion).not.toHaveBeenCalled();
},
);
test('should call onUserInteraction when user takes action', async () => {
await renderComponent();
const input = screen.getByTestId('content-answers-question-input');
input.focus();
await userEvent.keyboard('foo');
expect(mockProps.onUserInteraction).toHaveBeenCalled();
});
test('Should call onSelectedAgentCallback on agent selected change', async () => {
const mockOnSelectedAgentCallback = jest.fn();
await renderComponent({
onSelectedAgentCallback: mockOnSelectedAgentCallback,
});
expect(mockOnSelectedAgentCallback).toHaveBeenCalled();
});
});