import { Fragment } from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import Message from './Message';
import userEvent from '@testing-library/user-event';
import { monitorSampleAppQuickStart } from './QuickStarts/monitor-sampleapp-quickstart';
import { monitorSampleAppQuickStartWithImage } from './QuickStarts/monitor-sampleapp-quickstart-with-image';
import rehypeExternalLinks from '../__mocks__/rehype-external-links';
import { AlertActionLink, Button, CodeBlockAction } from '@patternfly/react-core';
import { DeepThinkingProps } from '../DeepThinking';
const ALL_ACTIONS = [
{ label: /Good response/i },
{ label: /Bad response/i },
{ label: /Copy/i },
{ label: /Edit/i },
{ label: /Share/i },
{ label: /Listen/i }
];
const UNORDERED_LIST = `
Here is an unordered list:
* Item 1
* Item 2
* Item 3
`;
const ORDERED_LIST = `
Here is an ordered list:
1. Item 1
2. Item 2
3. Item 3
`;
const CODE_MESSAGE = `
Here is some YAML code:
~~~yaml
apiVersion: helm.openshift.io/v1beta1/
kind: HelmChartRepository
metadata:
name: azure-sample-repo0oooo00ooo
spec:
connectionConfig:
url: https://raw.githubusercontent.com/Azure-Samples/helm-charts/master/docs
~~~`;
const CODE = `
apiVersion: helm.openshift.io/v1beta1/
kind: HelmChartRepository
metadata:
name: azure-sample-repo0oooo00ooo
spec:
connectionConfig:
url: https://raw.githubusercontent.com/Azure-Samples/helm-charts/master/docs
`;
const INLINE_CODE = `Here is an inline code - \`() => void\``;
const ORDERED_LIST_WITH_CODE = `
1. Item 1
2. Item 2
\`\`\`yaml
- name: Hello World Playbook
hosts: localhost
tasks:
- name: Print Hello World
ansible.builtin.debug:
msg: "Hello, World!"
\`\`\`
3. Item 3
`;
const HEADING = `
# h1 Heading
## h2 Heading
### h3 Heading
#### h4 Heading
##### h5 Heading
###### h6 Heading
`;
const BLOCK_QUOTES = `> Blockquotes can also be nested...
>> ...by using additional greater-than signs (>) right next to each other...
> > > ...or with spaces between each sign.`;
const TABLE = `
| Column 1 | Column 2 |
|-|-|
| Cell 1 | Cell 2 |
| Cell 3 | Cell 4 |
`;
const ONE_COLUMN_TABLE = `
| Column 1 |
|-|
| Cell 1 |
| Cell 2 |
`;
const ONE_CELL_TABLE = `
| Column 1 |
|-|
| Cell 1 |
`;
const HEADERLESS_TABLE = `
| |
|-|
| Cell 1 |
`;
const CHILDLESS_TABLE = `
| Column 1 |
|-|
| |
`;
const EMPTY_TABLE = `
| |
|-|
| |
`;
const FOOTNOTE = `This is some text with a footnote[^1] and here's a longer one.[^bignote]
You can also reference the same footnote multiple times[^1].
[^1]: This is the full footnote text. You can click the arrow to go back up.
[^bignote]: Here's one with multiple paragraphs and **formatting**.
Indent paragraphs to include them in the footnote.
Add as many paragraphs as you like. You can include *italic text*, **bold text**, and even \`code\`.
> You can even include blockquotes in footnotes!`;
const IMAGE = ``;
const INLINE_IMAGE = `inline text `;
const DEEP_THINKING: DeepThinkingProps = {
toggleContent: 'Show thinking',
subheading: 'Thought for 3 seconds',
body: "Here's why I said this."
};
const ERROR = {
title: 'Could not load chat',
children: 'Wait a few minutes and check your network settings. If the issue persists: ',
actionLinks: (
Start a new chat
Contact support
)
};
const checkListItemsRendered = () => {
const items = ['Item 1', 'Item 2', 'Item 3'];
expect(screen.getAllByRole('listitem')).toHaveLength(3);
items.forEach((item) => {
// list item text gets wrapped in a span by the third-party library so we can't just check the listitem
expect(screen.getByText(item)).toBeTruthy();
});
};
describe('Message', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should render user messages correctly', () => {
render( );
expect(screen.getByText('User')).toBeTruthy();
expect(screen.getByText('Hi')).toBeTruthy();
const date = new Date();
const formattedDate = date.toLocaleDateString();
expect(
screen.getByText((content, element) => {
const hasText = content.includes(formattedDate);
const isVisible = element?.tagName.toLowerCase() !== 'script' && element?.tagName.toLowerCase() !== 'style';
return hasText && isVisible;
})
).toBeInTheDocument();
expect(screen.queryByText('Loading message')).toBeFalsy();
expect(screen.getByRole('img')).toHaveAttribute('src', './img');
});
it('should render bot messages correctly', () => {
render( );
expect(screen.getByText('Bot')).toBeTruthy();
expect(screen.getByText('AI')).toBeTruthy();
expect(screen.getByText('Hi')).toBeTruthy();
const date = new Date();
const formattedDate = date.toLocaleDateString();
expect(
screen.getByText((content, element) => {
const hasText = content.includes(formattedDate);
const isVisible = element?.tagName.toLowerCase() !== 'script' && element?.tagName.toLowerCase() !== 'style';
return hasText && isVisible;
})
).toBeInTheDocument();
});
it('should render avatar correctly', () => {
render( );
expect(screen.getByRole('img')).toHaveAttribute('src', './testImg');
});
it('should not render avatar if no avatar prop is passed', () => {
render( );
expect(screen.queryByRole('img')).not.toBeInTheDocument();
});
it('should render botWord correctly', () => {
render( );
expect(screen.getByText('Bot')).toBeTruthy();
expect(screen.getByText('人工知能')).toBeTruthy();
expect(screen.queryByText('AI')).toBeFalsy();
expect(screen.getByText('Hi')).toBeTruthy();
});
it('should render timestamps', () => {
render( );
expect(screen.getByText('Bot')).toBeTruthy();
expect(screen.getByText('AI')).toBeTruthy();
expect(screen.getByText('Hi')).toBeTruthy();
expect(screen.getByText('2 hours ago')).toBeTruthy();
const date = new Date();
const formattedDate = date.toLocaleDateString();
expect(
screen.queryByText((content, element) => {
const hasText = content.includes(formattedDate);
const isVisible = element?.tagName.toLowerCase() !== 'script' && element?.tagName.toLowerCase() !== 'style';
return hasText && isVisible;
})
).not.toBeInTheDocument();
});
it('should render attachments', () => {
render( );
expect(screen.getByText('Hi')).toBeTruthy();
expect(screen.getByText('testAttachment')).toBeTruthy();
});
it('should be able to click attachments', async () => {
const spy = jest.fn();
render(
);
expect(screen.getByText('Hi')).toBeTruthy();
expect(screen.getByText('testAttachment')).toBeTruthy();
await userEvent.click(screen.getByRole('button', { name: /testAttachment/i }));
expect(spy).toHaveBeenCalledTimes(1);
});
it('should be able to close attachments', async () => {
const spy = jest.fn();
render(
);
expect(screen.getByText('Hi')).toBeTruthy();
expect(screen.getByText('testAttachment')).toBeTruthy();
expect(screen.getByRole('button', { name: /close testAttachment/i })).toBeTruthy();
await userEvent.click(screen.getByRole('button', { name: /close testAttachment/i }));
expect(spy).toHaveBeenCalledTimes(1);
});
it('should render loading state', () => {
render( );
expect(screen.getByText('Bot')).toBeTruthy();
expect(screen.getByText('AI')).toBeTruthy();
expect(screen.queryByText('Hi')).toBeFalsy();
const date = new Date();
const formattedDate = date.toLocaleDateString();
expect(
screen.getByText((content, element) => {
const hasText = content.includes(formattedDate);
const isVisible = element?.tagName.toLowerCase() !== 'script' && element?.tagName.toLowerCase() !== 'style';
return hasText && isVisible;
})
).toBeInTheDocument();
expect(screen.getByText('Loading message')).toBeTruthy();
});
it('should be able to show sources', async () => {
render(
);
expect(screen.getByText('Getting started with Red Hat OpenShift')).toBeTruthy();
});
it('should not show sources if loading', () => {
render(
);
expect(screen.getByText('Loading message')).toBeTruthy();
expect(screen.queryByText('Getting started with Red Hat OpenShift')).toBeFalsy();
});
it('should be able to show quick response', async () => {
const spy = jest.fn();
render(
);
const quickResponse = screen.getByRole('button', { name: /Yes/i });
expect(quickResponse).toBeTruthy();
await userEvent.click(quickResponse);
expect(spy).toHaveBeenCalledTimes(1);
});
it('should be able to handle isCompact', async () => {
render(
);
const parent = screen.getByRole('button', { name: /Yes/i }).parentNode;
expect(parent).toHaveClass('pf-m-compact');
});
it('should be able to show more than 1 quick response', async () => {
const spy = jest.fn();
render(
);
expect(screen.getByRole('button', { name: /Yes/i })).toBeTruthy();
expect(screen.getByRole('button', { name: /No/i })).toBeTruthy();
});
it('should be able to spread quickResponseContainerProps', async () => {
const spy = jest.fn();
render(
);
expect(screen.getByRole('button', { name: /Yes/i })).toBeTruthy();
expect(screen.queryByRole('button', { name: /No/i })).toBeFalsy();
expect(screen.getByRole('button', { name: /1 more/i }));
});
it('Renders response actions when a single actions object is passed', async () => {
render(
console.log('Good response') },
// eslint-disable-next-line no-console
negative: { onClick: () => console.log('Bad response') },
// eslint-disable-next-line no-console
copy: { onClick: () => console.log('Copy') },
// eslint-disable-next-line no-console
edit: { onClick: () => console.log('Edit') },
// eslint-disable-next-line no-console
share: { onClick: () => console.log('Share') },
// eslint-disable-next-line no-console
download: { onClick: () => console.log('Download') },
// eslint-disable-next-line no-console
listen: { onClick: () => console.log('Listen') }
}}
/>
);
ALL_ACTIONS.forEach(({ label }) => {
expect(screen.getByRole('button', { name: label })).toBeVisible();
});
});
it('Renders response actions when an array of actions objects is passed', async () => {
render(
console.log('Good response') },
// eslint-disable-next-line no-console
negative: { onClick: () => console.log('Bad response') }
},
{
// eslint-disable-next-line no-console
copy: { onClick: () => console.log('Copy') },
// eslint-disable-next-line no-console
edit: { onClick: () => console.log('Edit') },
// eslint-disable-next-line no-console
share: { onClick: () => console.log('Share') },
// eslint-disable-next-line no-console
download: { onClick: () => console.log('Download') }
},
{
// eslint-disable-next-line no-console
listen: { onClick: () => console.log('Listen') }
}
]}
/>
);
ALL_ACTIONS.forEach(({ label }) => {
expect(screen.getByRole('button', { name: label })).toBeVisible();
});
});
it('Renders response actions when an array of objects containing actions objects is passed', async () => {
render(
console.log('Good response') },
// eslint-disable-next-line no-console
negative: { onClick: () => console.log('Bad response') }
}
},
{
actions: {
// eslint-disable-next-line no-console
copy: { onClick: () => console.log('Copy') },
// eslint-disable-next-line no-console
edit: { onClick: () => console.log('Edit') },
// eslint-disable-next-line no-console
share: { onClick: () => console.log('Share') },
// eslint-disable-next-line no-console
download: { onClick: () => console.log('Download') }
}
},
{
actions: {
// eslint-disable-next-line no-console
listen: { onClick: () => console.log('Listen') }
}
}
]}
/>
);
ALL_ACTIONS.forEach(({ label }) => {
expect(screen.getByRole('button', { name: label })).toBeVisible();
});
});
it('should handle persistActionSelection correctly when a single actions object is passed', async () => {
render(
);
const goodBtn = screen.getByRole('button', { name: /Good response/i });
const badBtn = screen.getByRole('button', { name: /Bad response/i });
await userEvent.click(goodBtn);
expect(screen.getByRole('button', { name: /Good response recorded/i })).toHaveClass(
'pf-chatbot__button--response-action-clicked'
);
await userEvent.click(screen.getByText('Test message'));
expect(screen.getByRole('button', { name: /Good response recorded/i })).toHaveClass(
'pf-chatbot__button--response-action-clicked'
);
await userEvent.click(badBtn);
expect(screen.getByRole('button', { name: /Bad response recorded/i })).toHaveClass(
'pf-chatbot__button--response-action-clicked'
);
expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
});
it('should handle persistActionSelection correctly when an array of actions objects is passed', async () => {
render(
);
const goodBtn = screen.getByRole('button', { name: /Good response/i });
const copyBtn = screen.getByRole('button', { name: /Copy/i });
await userEvent.click(goodBtn);
expect(screen.getByRole('button', { name: /Good response recorded/i })).toHaveClass(
'pf-chatbot__button--response-action-clicked'
);
await userEvent.click(screen.getByText('Test message'));
expect(screen.getByRole('button', { name: /Good response recorded/i })).toHaveClass(
'pf-chatbot__button--response-action-clicked'
);
await userEvent.click(copyBtn);
expect(screen.getByRole('button', { name: /Copied/i })).toHaveClass('pf-chatbot__button--response-action-clicked');
await userEvent.click(screen.getByText('Test message'));
expect(screen.getByRole('button', { name: /Good response recorded/i })).toHaveClass(
'pf-chatbot__button--response-action-clicked'
);
expect(screen.getByRole('button', { name: /Copied/i })).toHaveClass('pf-chatbot__button--response-action-clicked');
});
it('should handle persistActionSelection correctly when an array of objects containing actions objects is passed', async () => {
render(
);
const goodBtn = screen.getByRole('button', { name: /Good response/i });
const copyBtn = screen.getByRole('button', { name: /Copy/i });
await userEvent.click(goodBtn);
expect(screen.getByRole('button', { name: /Good response recorded/i })).toHaveClass(
'pf-chatbot__button--response-action-clicked'
);
await userEvent.click(copyBtn);
expect(screen.getByRole('button', { name: /Copied/i })).toHaveClass('pf-chatbot__button--response-action-clicked');
await userEvent.click(screen.getByText('Test message'));
expect(screen.getByRole('button', { name: /Good response recorded/i })).toHaveClass(
'pf-chatbot__button--response-action-clicked'
);
expect(copyBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked');
});
it('should not show actions if loading', async () => {
render(
console.log('Good response') },
// eslint-disable-next-line no-console
negative: { onClick: () => console.log('Bad response') },
// eslint-disable-next-line no-console
copy: { onClick: () => console.log('Copy') },
// eslint-disable-next-line no-console
edit: { onClick: () => console.log('Edit') },
// eslint-disable-next-line no-console
share: { onClick: () => console.log('Share') },
// eslint-disable-next-line no-console
download: { onClick: () => console.log('Download') },
// eslint-disable-next-line no-console
listen: { onClick: () => console.log('Listen') }
}}
/>
);
expect(screen.getByText('Loading message')).toBeTruthy();
ALL_ACTIONS.forEach(({ label }) => {
expect(screen.queryByRole('button', { name: label })).toBeFalsy();
});
});
it('should not show actions if isEditable is true', async () => {
render(
console.log('Good response') },
// eslint-disable-next-line no-console
negative: { onClick: () => console.log('Bad response') },
// eslint-disable-next-line no-console
copy: { onClick: () => console.log('Copy') },
// eslint-disable-next-line no-console
edit: { onClick: () => console.log('Edit') },
// eslint-disable-next-line no-console
share: { onClick: () => console.log('Share') },
// eslint-disable-next-line no-console
download: { onClick: () => console.log('Download') },
// eslint-disable-next-line no-console
listen: { onClick: () => console.log('Listen') }
}}
/>
);
ALL_ACTIONS.forEach(({ label }) => {
expect(screen.queryByRole('button', { name: label })).toBeFalsy();
});
});
it('should render unordered lists correctly', () => {
render( );
expect(screen.getByText('Here is an unordered list:')).toBeTruthy();
checkListItemsRendered();
});
it('should render ordered lists correctly', () => {
render( );
expect(screen.getByText('Here is an ordered list:')).toBeTruthy();
checkListItemsRendered();
});
it('should render ordered lists correctly if there is interstitial content', () => {
render( );
checkListItemsRendered();
const list = screen.getAllByRole('list')[1];
expect(list).toHaveAttribute('start', '3');
});
it('should render inline code', () => {
render( );
expect(screen.getByText(/() => void/i)).toBeTruthy();
expect(screen.queryByRole('button', { name: 'Copy code button' })).toBeFalsy();
});
it('should render code correctly', () => {
render( );
expect(screen.getByText('Here is some YAML code:')).toBeTruthy();
expect(screen.getByRole('button', { name: 'Copy code' })).toBeTruthy();
expect(screen.getByText(/yaml/)).toBeTruthy();
expect(screen.getByText(/apiVersion:/i)).toBeTruthy();
expect(screen.getByText(/helm.openshift.io\/v1beta1/i)).toBeTruthy();
expect(screen.getByText(/metadata:/i)).toBeTruthy();
expect(screen.getByText(/name:/i)).toBeTruthy();
expect(screen.getByText(/azure-sample-repo0oooo00ooo/i)).toBeTruthy();
expect(screen.getByText(/spec/i)).toBeTruthy();
expect(screen.getByText(/connectionConfig:/i)).toBeTruthy();
expect(screen.getByText(/url:/i)).toBeTruthy();
expect(
screen.getByText(/https:\/\/raw.githubusercontent.com\/Azure-Samples\/helm-charts\/master\/docs/i)
).toBeTruthy();
});
it('should render expandable code correctly', () => {
render(
);
expect(screen.getByText('Here is some YAML code:')).toBeTruthy();
expect(screen.getByRole('button', { name: 'Copy code' })).toBeTruthy();
expect(screen.getByText(/yaml/)).toBeTruthy();
expect(screen.getByText(/apiVersion/i)).toBeTruthy();
expect(screen.getByRole('button', { name: /Show more/i })).toBeTruthy();
});
it('should handle click on expandable code correctly', async () => {
render(
);
const button = screen.getByRole('button', { name: /Show more/i });
await userEvent.click(button);
expect(screen.getByRole('button', { name: /Show less/i })).toBeTruthy();
expect(screen.getByText(/yaml/)).toBeTruthy();
expect(screen.getByText(/apiVersion:/i)).toBeTruthy();
expect(screen.getByText(/helm.openshift.io\/v1beta1/i)).toBeTruthy();
expect(screen.getByText(/metadata:/i)).toBeTruthy();
expect(screen.getByText(/name:/i)).toBeTruthy();
expect(screen.getByText(/azure-sample-repo0oooo00ooo/i)).toBeTruthy();
expect(screen.getByText(/spec/i)).toBeTruthy();
expect(screen.getByText(/connectionConfig:/i)).toBeTruthy();
expect(screen.getByText(/url:/i)).toBeTruthy();
expect(
screen.getByText(/https:\/\/raw.githubusercontent.com\/Azure-Samples\/helm-charts\/master\/docs/i)
).toBeTruthy();
});
it('can click copy code button', async () => {
// need explicit setup since RTL stubs clipboard if you do this
const user = userEvent.setup();
render( );
expect(screen.getByRole('button', { name: 'Copy code' })).toBeTruthy();
await user.click(screen.getByRole('button', { name: 'Copy code' }));
const clipboardText = await navigator.clipboard.readText();
expect(clipboardText.trim()).toEqual(CODE.trim());
});
it('should handle codeBlockProps correctly by spreading it onto the CodeMessage', () => {
render(
);
expect(screen.getByRole('button', { name: 'test' })).toBeTruthy();
});
it('should be able to add custom actions to CodeMessage', () => {
render(
New custom action
)
}}
/>
);
expect(screen.getByRole('button', { name: /New custom action/i })).toBeTruthy();
});
it('should handle hasRoundAvatar correctly when it is true', () => {
render( );
expect(screen.getByRole('img')).toBeTruthy();
expect(screen.getByRole('img')).toHaveClass('pf-chatbot__message-avatar');
expect(screen.getByRole('img')).toHaveClass('pf-chatbot__message-avatar--round');
});
it('should handle hasRoundAvatar correctly when it is false', () => {
render( );
expect(screen.getByRole('img')).toBeTruthy();
expect(screen.getByRole('img')).toHaveClass('pf-chatbot__message-avatar');
expect(screen.getByRole('img')).not.toHaveClass('pf-chatbot__message-avatar--round');
});
it('should handle avatarProps correctly by spreading it onto the Message Avatar', () => {
render( );
expect(screen.getByRole('img')).toBeTruthy();
expect(screen.getByRole('img')).toHaveClass('test');
});
it('should handle avatarProps and hasRoundAvatar correctly', () => {
render(
);
expect(screen.getByRole('img')).toBeTruthy();
expect(screen.getByRole('img')).toHaveClass('test');
expect(screen.getByRole('img')).toHaveClass('pf-chatbot__message-avatar');
});
it('should handle QuickStart tile correctly', () => {
render(
alert(id)
}}
/>
);
expect(screen.getByRole('button', { name: 'Monitoring your sample application' })).toBeTruthy();
expect(screen.getByRole('heading', { name: '1 Prerequisite' })).toBeTruthy();
expect(screen.getByRole('button', { name: 'Show prerequisites' })).toBeTruthy();
expect(screen.getByRole('button', { name: 'Start' })).toBeTruthy();
});
it('should handle click on QuickStart tile correctly', async () => {
const spy = jest.fn();
render(
spy(id)
}}
/>
);
await userEvent.click(screen.getByRole('button', { name: 'Monitoring your sample application' }));
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith(monitorSampleAppQuickStart.metadata.name);
});
it('should handle QuickStart tile with image correctly', async () => {
const spy = jest.fn();
render(
spy(id)
}}
/>
);
expect(screen.getAllByRole('img')[1]).toHaveAttribute('src', 'test.png');
});
it('should handle tool response correctly', async () => {
render(
);
expect(screen.getByRole('button', { name: /Tool response: Name/i })).toBeTruthy();
expect(screen.getByText('Thought for 3 seconds')).toBeTruthy();
expect(screen.getByText('Lorem ipsum dolor sit amet')).toBeTruthy();
expect(screen.getByText('Card title')).toBeTruthy();
expect(screen.getByText('Card body')).toBeTruthy();
});
it('should handle block quote correctly', () => {
render( );
expect(screen.getByText(/Blockquotes can also be nested.../)).toBeTruthy();
expect(screen.getByText('...by using additional greater-than signs (>) right next to each other...')).toBeTruthy();
expect(screen.getByText(/...or with spaces between each sign./)).toBeTruthy();
});
it('should handle heading correctly', () => {
render( );
expect(screen.getByRole('heading', { name: /h1 Heading/i })).toBeTruthy();
expect(screen.getByRole('heading', { name: /h2 Heading/i })).toBeTruthy();
expect(screen.getByRole('heading', { name: /h3 Heading/i })).toBeTruthy();
expect(screen.getByRole('heading', { name: /h4 Heading/i })).toBeTruthy();
expect(screen.getByRole('heading', { name: /h5 Heading/i })).toBeTruthy();
expect(screen.getByRole('heading', { name: /h6 Heading/i })).toBeTruthy();
});
it('should render table correctly', () => {
render( );
expect(screen.getByRole('row', { name: /Column 1 Column 2/i })).toBeTruthy();
expect(screen.getByRole('row', { name: /Cell 1 Cell 2/i })).toBeTruthy();
expect(screen.getByRole('row', { name: /Cell 3 Cell 4/i })).toBeTruthy();
expect(screen.getByRole('columnheader', { name: /Column 1/i })).toBeTruthy();
expect(screen.getByRole('columnheader', { name: /Column 2/i })).toBeTruthy();
expect(screen.getByRole('cell', { name: /Cell 1/i })).toBeTruthy();
expect(screen.getByRole('cell', { name: /Cell 2/i })).toBeTruthy();
expect(screen.getByRole('cell', { name: /Cell 3/i })).toBeTruthy();
expect(screen.getByRole('cell', { name: /Cell 4/i })).toBeTruthy();
});
it('should render table data labels correctly for mobile breakpoint', () => {
render( );
expect(screen.getByRole('row', { name: /Cell 1 Cell 2/i })).toHaveAttribute('extraHeaders', 'Column 1,Column 2');
expect(screen.getByRole('row', { name: /Cell 3 Cell 4/i })).toHaveAttribute('extraHeaders', 'Column 1,Column 2');
expect(screen.getByRole('cell', { name: /Cell 1/i })).toHaveAttribute('data-label', 'Column 1');
expect(screen.getByRole('cell', { name: /Cell 2/i })).toHaveAttribute('data-label', 'Column 2');
expect(screen.getByRole('cell', { name: /Cell 3/i })).toHaveAttribute('data-label', 'Column 1');
expect(screen.getByRole('cell', { name: /Cell 4/i })).toHaveAttribute('data-label', 'Column 2');
});
it('should render table data labels correctly for mobile breakpoint for one column table', () => {
render( );
expect(screen.getByRole('row', { name: /Cell 1/i })).toHaveAttribute('extraHeaders', 'Column 1');
expect(screen.getByRole('row', { name: /Cell 2/i })).toHaveAttribute('extraHeaders', 'Column 1');
expect(screen.getByRole('cell', { name: /Cell 1/i })).toHaveAttribute('data-label', 'Column 1');
expect(screen.getByRole('cell', { name: /Cell 2/i })).toHaveAttribute('data-label', 'Column 1');
});
it('should render table data labels correctly for mobile breakpoint for one cell table', () => {
render( );
expect(screen.getByRole('row', { name: /Cell 1/i })).toHaveAttribute('extraHeaders', 'Column 1');
expect(screen.getByRole('cell', { name: /Cell 1/i })).toHaveAttribute('data-label', 'Column 1');
});
it('should render table data labels correctly for mobile breakpoint for headerless', () => {
render( );
expect(screen.getByRole('row', { name: /Cell 1/i })).toHaveAttribute('extraHeaders', '');
expect(screen.getByRole('cell', { name: /Cell 1/i })).not.toHaveAttribute('data-label');
});
it('should render table data labels correctly for mobile breakpoint for childless', () => {
render( );
expect(screen.getByRole('cell')).not.toHaveAttribute('extraHeaders', 'Column 1');
});
it('should render table data labels correctly for mobile breakpoint for empty', () => {
render( );
expect(screen.getByRole('cell')).not.toHaveAttribute('extraHeaders', '');
});
it('should render custom table aria label correctly', () => {
render( );
expect(screen.getByRole('grid', { name: /Test/i })).toBeTruthy();
});
it('should render footnote correctly', () => {
render( );
expect(screen.getByText(/This is some text with a footnote/i)).toBeTruthy();
expect(screen.getByText(/and here's a longer one./i)).toBeTruthy();
expect(screen.getByText(/You can also reference the same footnote multiple times./i)).toBeTruthy();
expect(screen.getByRole('heading', { name: /Footnotes/i })).toBeTruthy();
expect(screen.getByText(/This is the full footnote text. You can click the arrow to go back up./i)).toBeTruthy();
expect(screen.getByText(/Here's one with multiple paragraphs and/i)).toBeTruthy();
expect(screen.getByText(/formatting/i)).toBeTruthy();
expect(screen.getByText(/Indent paragraphs to include them in the footnote./i)).toBeTruthy();
expect(screen.getByText(/Add as many paragraphs as you like. You can include/i)).toBeTruthy();
expect(screen.getByText(/italic text/i)).toBeTruthy();
expect(screen.getByText(/bold text/i)).toBeTruthy();
expect(screen.getByText(/, and even/i)).toBeTruthy();
expect(screen.getByText(/code/i)).toBeTruthy();
expect(screen.getByText(/You can even include blockquotes in footnotes!/i)).toBeTruthy();
expect(screen.getAllByRole('link', { name: '1' })).toHaveLength(2);
expect(screen.getAllByRole('link', { name: '2' })).toBeTruthy();
expect(screen.getByRole('link', { name: 'Back to reference 1' })).toBeTruthy();
expect(screen.getByRole('link', { name: 'Back to reference 1-2' })).toBeTruthy();
expect(screen.getByRole('link', { name: /Back to reference 2/i })).toBeTruthy();
});
it('should render beforeMainContent with main content', () => {
const mainContent = 'Main message content';
const beforeMainContentText = 'Before main content';
const beforeMainContent = {beforeMainContentText}
;
render(
);
expect(screen.getByText(beforeMainContentText)).toBeTruthy();
expect(screen.getByText(mainContent)).toBeTruthy();
});
it('should render afterMainContent with main content', () => {
const mainContent = 'Main message content';
const afterMainContentText = 'After main content';
const afterMainContent = {afterMainContentText}
;
render(
);
expect(screen.getByText(afterMainContentText)).toBeTruthy();
expect(screen.getByText(mainContent)).toBeTruthy();
});
it('should render endContent with main content', () => {
const mainContent = 'Main message content';
const endMainContentText = 'End content';
const endContent = {endMainContentText}
;
render( );
expect(screen.getByText(endMainContentText)).toBeTruthy();
expect(screen.getByText(mainContent)).toBeTruthy();
});
it('should render all parts of extraContent with main content', () => {
const beforeMainContent = Before main content
;
const afterMainContent = After main content
;
const endContent = End content
;
render(
);
expect(screen.getByText('Before main content')).toBeTruthy();
expect(screen.getByText('Main message content')).toBeTruthy();
expect(screen.getByText('After main content')).toBeTruthy();
expect(screen.getByText('End content')).toBeTruthy();
});
it('should not render extraContent when not provided', () => {
render( );
// Ensure no extraContent is rendered
expect(screen.getByText('Main message content')).toBeTruthy();
expect(screen.queryByText('Before main content')).toBeFalsy();
expect(screen.queryByText('After main content')).toBeFalsy();
expect(screen.queryByText('end message content')).toBeFalsy();
});
it('should handle undefined or null values in extraContent gracefully', () => {
render(
);
// Ensure that no extraContent is rendered if they are null or undefined
expect(screen.getByText('Main message content')).toBeTruthy();
expect(screen.queryByText('Before main content')).toBeFalsy();
expect(screen.queryByText('After main content')).toBeFalsy();
expect(screen.queryByText('end message content')).toBeFalsy();
});
it('should render JSX in extraContent correctly', () => {
const beforeMainContent = (
Bold before content
);
const afterMainContent = (
Bold after content
);
const endContent = (
Bold end content
);
render(
);
// Check that the JSX is correctly rendered
expect(screen.getByTestId('before-main-content')).toContainHTML('Bold before content ');
expect(screen.getByTestId('after-main-content')).toContainHTML('Bold after content ');
expect(screen.getByTestId('end-main-content')).toContainHTML('Bold end content ');
});
it('should handle image correctly for user', () => {
render( );
expect(screen.queryByRole('img', { name: /Multi-colored wavy lines on a black background/i })).toBeFalsy();
});
it('should handle image correctly for bot', () => {
render( );
expect(screen.getByRole('img', { name: /Multi-colored wavy lines on a black background/i })).toBeTruthy();
});
it('inline image parent should have class pf-chatbot__message-and-actions', () => {
render( );
expect(screen.getByRole('img', { name: /Multi-colored wavy lines on a black background/i })).toBeTruthy();
expect(
screen.getByRole('img', { name: /Multi-colored wavy lines on a black background/i }).parentElement
).toHaveClass('pf-chatbot__message-and-actions');
});
it('should handle external links correctly', () => {
render( );
// we are mocking rehype libraries, so we can't test target _blank addition on links directly with RTL
expect(rehypeExternalLinks).toHaveBeenCalledTimes(1);
});
it('should handle external links correctly', () => {
render(
);
// we are mocking rehype libraries, so we can't test target _blank addition on links directly with RTL
expect(rehypeExternalLinks).not.toHaveBeenCalled();
});
it('should handle extra link props correctly', async () => {
const spy = jest.fn();
render(
);
await userEvent.click(screen.getByRole('link', { name: /PatternFly/i }));
expect(spy).toHaveBeenCalledTimes(1);
});
it('should handle error correctly', () => {
render( );
expect(screen.getByRole('heading', { name: /Could not load chat/i })).toBeTruthy();
expect(screen.getByRole('link', { name: /Start a new chat/i })).toBeTruthy();
expect(screen.getByRole('link', { name: /Contact support/i })).toBeTruthy();
expect(screen.getByText('Wait a few minutes and check your network settings. If the issue persists:')).toBeTruthy();
});
it('should handle error correctly when loading', () => {
render( );
expect(screen.queryByRole('heading', { name: /Could not load chat/i })).toBeFalsy();
expect(screen.getByText('Loading message')).toBeTruthy();
});
it('should handle error correctly when these is content', () => {
render( );
expect(screen.getByRole('heading', { name: /Could not load chat/i })).toBeTruthy();
expect(screen.queryByText('Test')).toBeFalsy();
});
it('should handle isEditable when there is message content', () => {
render( );
expect(screen.getByRole('textbox')).toBeTruthy();
expect(screen.getByRole('textbox')).toHaveValue('Test');
expect(screen.getByRole('button', { name: /Update/i })).toBeTruthy();
expect(screen.getByRole('button', { name: /Cancel/i })).toBeTruthy();
});
it('should handle isEditable when there is no message content', () => {
render( );
expect(screen.getByRole('textbox')).toBeTruthy();
expect(screen.getByRole('textbox')).toHaveValue('');
expect(screen.getByRole('textbox')).toHaveAttribute('placeholder', 'Edit prompt message...');
expect(screen.getByRole('button', { name: /Update/i })).toBeTruthy();
expect(screen.getByRole('button', { name: /Cancel/i })).toBeTruthy();
});
it('should be able to change edit placeholder', () => {
render( );
expect(screen.getByRole('textbox')).toBeTruthy();
expect(screen.getByRole('textbox')).toHaveValue('');
expect(screen.getByRole('textbox')).toHaveAttribute('placeholder', 'I am a placeholder');
});
it('should be able to change updateWord', () => {
render( );
expect(screen.getByRole('button', { name: /Submit/i })).toBeTruthy();
});
it('should be able to change cancelWord', () => {
render( );
expect(screen.getByRole('button', { name: /Don't submit/i })).toBeTruthy();
});
it('should be able to add onEditUpdate', async () => {
const spy = jest.fn();
render( );
await userEvent.click(screen.getByRole('button', { name: /Update/i }));
expect(spy).toHaveBeenCalledTimes(1);
});
it('should be able to add onEditCancel', async () => {
const spy = jest.fn();
render( );
await userEvent.click(screen.getByRole('button', { name: /Cancel/i }));
expect(spy).toHaveBeenCalledTimes(1);
});
it('should be able to add editFormProps', () => {
const { container } = render(
);
const form = container.querySelector('form');
expect(form).toHaveClass('test');
});
it('should be able to disable markdown parsing', () => {
render( );
// this is looking for markdown syntax that is ordinarily stripped
expect(screen.getByText(/~~~yaml/i)).toBeTruthy();
});
it('should be able to pass props to react-markdown, such as disabling tags', () => {
render(
);
expect(screen.getByText('Here is some YAML code:')).toBeTruthy();
// code block isn't rendering
expect(screen.queryByRole('button', { name: 'Copy code' })).toBeFalsy();
});
it('should disable images and additional tags for user messages', () => {
render(
);
expect(screen.getByText('Here is some YAML code:')).toBeTruthy();
// code block isn't rendering
expect(screen.queryByRole('button', { name: 'Copy code' })).toBeFalsy();
expect(screen.queryByRole('img', { name: /Multi-colored wavy lines on a black background/i })).toBeFalsy();
});
it('can override image tag removal default for user messages', () => {
render( );
expect(screen.getByRole('img', { name: /Multi-colored wavy lines on a black background/i })).toBeTruthy();
});
it('should render deep thinking section correctly', () => {
render( );
expect(screen.getByRole('button', { name: /Show thinking/i })).toBeTruthy();
expect(screen.getByText('Thought for 3 seconds')).toBeTruthy();
expect(screen.getByText("Here's why I said this.")).toBeTruthy();
});
it('should handle isPrimary correctly for inline code when it is true', () => {
const { container } = render( );
expect(container.querySelector('.pf-m-primary')).toBeTruthy();
});
it('should handle isPrimary correctly for inline code when it is false', () => {
const { container } = render( );
expect(container.querySelector('.pf-m-primary')).toBeFalsy();
});
it('should handle isPrimary correctly for table when it is true', () => {
const { container } = render( );
expect(container.querySelector('.pf-m-primary')).toBeTruthy();
});
it('should handle isPrimary correctly for table when it is false', () => {
const { container } = render( );
expect(container.querySelector('.pf-m-primary')).toBeFalsy();
});
it('should handle isPrimary correctly for loading when it is true', () => {
const { container } = render( );
expect(container.querySelector('.pf-m-primary')).toBeTruthy();
});
it('should handle isPrimary correctly for loading when it is false', () => {
const { container } = render( );
expect(container.querySelector('.pf-m-primary')).toBeFalsy();
});
it('should handle isPrimary correctly for attachments when it is true', () => {
const { container } = render(
);
expect(container.querySelector('.pf-m-outline')).toBeTruthy();
});
it('should handle isPrimary correctly for attachments when it is false', () => {
const { container } = render(
);
expect(container.querySelector('.pf-m-outline')).toBeFalsy();
});
});