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 = `![Multi-colored wavy lines on a black background](https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif)`; const INLINE_IMAGE = `inline text ![Multi-colored wavy lines on a black background](https://cdn.dribbble.com/userupload/10651749/file/original-8a07b8e39d9e8bf002358c66fce1223e.gif)`; 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( ) }} /> ); 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(); }); });