import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; import ResponseActions, { ActionProps } from './ResponseActions'; import userEvent from '@testing-library/user-event'; import { DownloadIcon, InfoCircleIcon, RedoIcon } from '@patternfly/react-icons'; import Message from '../Message'; const ALL_ACTIONS = [ { type: 'positive', label: 'Good response', clickedLabel: 'Good response recorded' }, { type: 'negative', label: 'Bad response', clickedLabel: 'Bad response recorded' }, { type: 'copy', label: 'Copy', clickedLabel: 'Copied' }, { type: 'edit', label: 'Edit', clickedLabel: 'Editing' }, { type: 'share', label: 'Share', clickedLabel: 'Shared' }, { type: 'listen', label: 'Listen', clickedLabel: 'Listening' } ]; const CUSTOM_ACTIONS = [ { regenerate: { ariaLabel: 'Regenerate', clickedAriaLabel: 'Regenerated', onClick: jest.fn(), tooltipContent: 'Regenerate', clickedTooltipContent: 'Regenerated', icon: }, download: { ariaLabel: 'Download', clickedAriaLabel: 'Downloaded', onClick: jest.fn(), tooltipContent: 'Download', clickedTooltipContent: 'Downloaded', icon: }, info: { ariaLabel: 'Info', onClick: jest.fn(), tooltipContent: 'Info', icon: } } ]; const ALL_ACTIONS_DATA_TEST = [ { type: 'positive', label: 'Good response', dataTestId: 'positive' }, { type: 'negative', label: 'Bad response', dataTestId: 'negative' }, { type: 'copy', label: 'Copy', dataTestId: 'copy' }, { type: 'edit', label: 'Edit', dataTestId: 'edit' }, { type: 'share', label: 'Share', dataTestId: 'share' }, { type: 'download', label: 'Download', dataTestId: 'download' }, { type: 'listen', label: 'Listen', dataTestId: 'listen' } ]; describe('ResponseActions', () => { afterEach(() => { jest.clearAllMocks(); }); it('should handle click within group of buttons correctly', async () => { render( ); const goodBtn = screen.getByRole('button', { name: 'Good response' }); const badBtn = screen.getByRole('button', { name: 'Bad response' }); const copyBtn = screen.getByRole('button', { name: 'Copy' }); const editBtn = screen.getByRole('button', { name: 'Edit' }); const shareBtn = screen.getByRole('button', { name: 'Share' }); const downloadBtn = screen.getByRole('button', { name: 'Download' }); const listenBtn = screen.getByRole('button', { name: 'Listen' }); const buttons = [goodBtn, badBtn, copyBtn, editBtn, shareBtn, downloadBtn, listenBtn]; buttons.forEach((button) => { expect(button).toBeTruthy(); }); await userEvent.click(goodBtn); expect(screen.getByRole('button', { name: 'Good response recorded' })).toHaveClass( 'pf-chatbot__button--response-action-clicked' ); let unclickedButtons = buttons.filter((button) => button !== goodBtn); unclickedButtons.forEach((button) => { expect(button).not.toHaveClass('pf-chatbot__button--response-action-clicked'); }); await userEvent.click(badBtn); expect(screen.getByRole('button', { name: 'Bad response recorded' })).toHaveClass( 'pf-chatbot__button--response-action-clicked' ); unclickedButtons = buttons.filter((button) => button !== badBtn); unclickedButtons.forEach((button) => { expect(button).not.toHaveClass('pf-chatbot__button--response-action-clicked'); }); }); it('should handle click outside of group of buttons correctly', async () => { // using message just so we have something outside the group that's rendered render( ); const goodBtn = screen.getByRole('button', { name: 'Good response' }); const badBtn = screen.getByRole('button', { name: 'Bad response' }); expect(goodBtn).toBeTruthy(); expect(badBtn).toBeTruthy(); await userEvent.click(goodBtn); expect(screen.getByRole('button', { name: 'Good response recorded' })).toHaveClass( 'pf-chatbot__button--response-action-clicked' ); expect(badBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked'); await userEvent.click(badBtn); expect(screen.getByRole('button', { name: 'Bad response recorded' })).toHaveClass( 'pf-chatbot__button--response-action-clicked' ); expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked'); await userEvent.click( screen.getByText("I updated your account with those settings. You're ready to set up your first dashboard!") ); expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked'); expect(badBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked'); }); it('should handle isClicked prop within group of buttons correctly', async () => { render( } /> ); expect(screen.getByTestId('positive-btn')).toHaveClass('pf-chatbot__button--response-action-clicked'); expect(screen.getByTestId('negative-btn')).not.toHaveClass('pf-chatbot__button--response-action-clicked'); }); it('should set "listen" button as active if its `isClicked` is true', async () => { render( } /> ); expect(screen.getByTestId('listen-btn')).toHaveClass('pf-chatbot__button--response-action-clicked'); expect(screen.getByTestId('positive-btn')).not.toHaveClass('pf-chatbot__button--response-action-clicked'); expect(screen.getByTestId('negative-btn')).not.toHaveClass('pf-chatbot__button--response-action-clicked'); }); it('should prioritize "positive" when both "positive" and "negative" are set to clicked', async () => { render( } /> ); // Positive button should take precendence expect(screen.getByTestId('positive-btn')).toHaveClass('pf-chatbot__button--response-action-clicked'); expect(screen.getByTestId('negative-btn')).not.toHaveClass('pf-chatbot__button--response-action-clicked'); }); it('should set an additional action button as active if it is initially clicked and no predefined are clicked', async () => { const [additionalActions] = CUSTOM_ACTIONS; const customActions = { positive: { 'data-testid': 'positive', onClick: jest.fn(), isClicked: false }, negative: { 'data-testid': 'negative', onClick: jest.fn(), isClicked: false }, ...Object.keys(additionalActions).reduce((acc, actionKey) => { acc[actionKey] = { ...additionalActions[actionKey], 'data-testid': actionKey, isClicked: actionKey === 'regenerate' }; return acc; }, {}) }; render(); Object.keys(customActions).forEach((actionKey) => { if (actionKey === 'regenerate') { expect(screen.getByTestId(actionKey)).toHaveClass('pf-chatbot__button--response-action-clicked'); } else { // Other actions should not have clicked class expect(screen.getByTestId(actionKey)).not.toHaveClass('pf-chatbot__button--response-action-clicked'); } }); }); it('should activate the clicked button and deactivate any previously active button', async () => { const actions = { positive: { 'data-testid': 'positive', onClick: jest.fn(), isClicked: false }, negative: { 'data-testid': 'negative', onClick: jest.fn(), isClicked: true } }; render(); const negativeBtn = screen.getByTestId('negative'); const positiveBtn = screen.getByTestId('positive'); // negative button is initially clicked expect(negativeBtn).toHaveClass('pf-chatbot__button--response-action-clicked'); expect(positiveBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked'); await userEvent.click(positiveBtn); // positive button should now have the clicked class expect(positiveBtn).toHaveClass('pf-chatbot__button--response-action-clicked'); expect(negativeBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked'); }); it('should render buttons correctly', () => { ALL_ACTIONS.forEach(({ type, label }) => { render(); expect(screen.getByRole('button', { name: label })).toBeTruthy(); }); }); it('should be able to call onClick correctly', async () => { for (const { type, label } of ALL_ACTIONS) { const spy = jest.fn(); render(); await userEvent.click(screen.getByRole('button', { name: label })); expect(spy).toHaveBeenCalledTimes(1); } }); it('should swap clicked and non-clicked aria labels on click', async () => { for (const { type, label, clickedLabel } of ALL_ACTIONS) { render(); expect(screen.getByRole('button', { name: label })).toBeTruthy(); await userEvent.click(screen.getByRole('button', { name: label })); expect(screen.getByRole('button', { name: clickedLabel })).toBeTruthy(); } }); it('should swap clicked and non-clicked tooltips on click', async () => { for (const { type, label, clickedLabel } of ALL_ACTIONS) { render(); expect(screen.getByRole('button', { name: label })).toBeTruthy(); await userEvent.click(screen.getByRole('button', { name: label })); expect(screen.getByRole('tooltip', { name: clickedLabel })).toBeTruthy(); } }); it('should be able to change aria labels', () => { const actions = [ { type: 'positive', ariaLabel: 'Thumbs up' }, { type: 'negative', ariaLabel: 'Thumbs down' }, { type: 'copy', ariaLabel: 'Copy the message' }, { type: 'edit', ariaLabel: 'Edit this message' }, { type: 'share', ariaLabel: 'Share it with friends' }, { type: 'download', ariaLabel: 'Download your cool message' }, { type: 'listen', ariaLabel: 'Listen up' } ]; actions.forEach(({ type, ariaLabel }) => { render(); expect(screen.getByRole('button', { name: ariaLabel })).toBeTruthy(); }); }); it('should be able to disable buttons', () => { ALL_ACTIONS.forEach(({ type, label }) => { render(); expect(screen.getByRole('button', { name: label })).toBeDisabled(); }); }); it('should be able to add class to buttons', () => { ALL_ACTIONS.forEach(({ type, label }) => { render(); expect(screen.getByRole('button', { name: label })).toHaveClass('test'); }); }); it('should be able to add custom attributes to buttons', () => { ALL_ACTIONS_DATA_TEST.forEach(({ type, dataTestId }) => { render(); expect(screen.getByTestId(dataTestId)).toBeTruthy(); }); }); it('should be able to add custom actions', () => { CUSTOM_ACTIONS.forEach((action) => { const key = Object.keys(action)[0]; render( ); expect(screen.getByRole('button', { name: key })).toBeTruthy(); expect(screen.getByTestId(action[key])).toBeTruthy(); }); }); // we are testing for the reverse case already above it('should not deselect when clicking outside when persistActionSelection is true', async () => { render( ); const goodBtn = screen.getByRole('button', { name: 'Good response' }); await userEvent.click(goodBtn); expect(screen.getByRole('button', { name: 'Good response recorded' })).toHaveClass( 'pf-chatbot__button--response-action-clicked' ); await userEvent.click(screen.getByText('Test content')); expect(screen.getByRole('button', { name: 'Good response recorded' })).toHaveClass( 'pf-chatbot__button--response-action-clicked' ); }); it('should switch selection to another button when persistActionSelection is true', async () => { render( ); const goodBtn = screen.getByRole('button', { name: 'Good response' }); const badBtn = screen.getByRole('button', { name: 'Bad response' }); await userEvent.click(goodBtn); expect(goodBtn).toHaveClass('pf-chatbot__button--response-action-clicked'); await userEvent.click(badBtn); expect(badBtn).toHaveClass('pf-chatbot__button--response-action-clicked'); expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked'); }); it('should toggle off when clicking the same button when persistActionSelection is true', async () => { render( ); const goodBtn = screen.getByRole('button', { name: 'Good response' }); await userEvent.click(goodBtn); expect(goodBtn).toHaveClass('pf-chatbot__button--response-action-clicked'); await userEvent.click(goodBtn); expect(goodBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked'); }); it('should work with custom actions when persistActionSelection is true', async () => { const actions = { positive: { 'data-testid': 'positive', onClick: jest.fn() }, negative: { 'data-testid': 'negative', onClick: jest.fn() }, custom: { 'data-testid': 'custom', onClick: jest.fn(), ariaLabel: 'Custom', tooltipContent: 'Custom action', icon: } }; render(); const customBtn = screen.getByTestId('custom'); await userEvent.click(customBtn); expect(customBtn).toHaveClass('pf-chatbot__button--response-action-clicked'); await userEvent.click(customBtn); expect(customBtn).not.toHaveClass('pf-chatbot__button--response-action-clicked'); }); });