@patternfly/chatbot 6.4.0-prerelease.20 → 6.4.0-prerelease.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/cjs/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.js +1 -1
  2. package/dist/cjs/FileDetails/FileDetails.d.ts +22 -3
  3. package/dist/cjs/FileDetails/FileDetails.js +27 -912
  4. package/dist/cjs/FileDetails/FileDetails.test.js +16 -0
  5. package/dist/cjs/FileDetailsLabel/FileDetailsLabel.d.ts +8 -2
  6. package/dist/cjs/FileDetailsLabel/FileDetailsLabel.js +14 -2
  7. package/dist/cjs/FileDetailsLabel/FileDetailsLabel.test.js +19 -1
  8. package/dist/cjs/ImagePreview/ImagePreview.d.ts +53 -0
  9. package/dist/cjs/ImagePreview/ImagePreview.js +47 -0
  10. package/dist/cjs/ImagePreview/ImagePreview.test.d.ts +1 -0
  11. package/dist/cjs/ImagePreview/ImagePreview.test.js +225 -0
  12. package/dist/cjs/ImagePreview/index.d.ts +2 -0
  13. package/dist/cjs/ImagePreview/index.js +23 -0
  14. package/dist/cjs/Message/Message.d.ts +3 -0
  15. package/dist/cjs/Message/Message.js +3 -2
  16. package/dist/cjs/MessageBox/MessageBox.js +1 -1
  17. package/dist/cjs/ToolCall/ToolCall.d.ts +44 -0
  18. package/dist/cjs/ToolCall/ToolCall.js +14 -0
  19. package/dist/cjs/ToolCall/ToolCall.test.d.ts +1 -0
  20. package/dist/cjs/ToolCall/ToolCall.test.js +144 -0
  21. package/dist/cjs/ToolCall/index.d.ts +2 -0
  22. package/dist/cjs/ToolCall/index.js +23 -0
  23. package/dist/cjs/index.d.ts +4 -0
  24. package/dist/cjs/index.js +7 -1
  25. package/dist/css/main.css +104 -19
  26. package/dist/css/main.css.map +1 -1
  27. package/dist/dynamic/ImagePreview/package.json +1 -0
  28. package/dist/dynamic/ToolCall/package.json +1 -0
  29. package/dist/esm/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.js +1 -1
  30. package/dist/esm/FileDetails/FileDetails.d.ts +22 -3
  31. package/dist/esm/FileDetails/FileDetails.js +27 -912
  32. package/dist/esm/FileDetails/FileDetails.test.js +16 -0
  33. package/dist/esm/FileDetailsLabel/FileDetailsLabel.d.ts +8 -2
  34. package/dist/esm/FileDetailsLabel/FileDetailsLabel.js +14 -2
  35. package/dist/esm/FileDetailsLabel/FileDetailsLabel.test.js +19 -1
  36. package/dist/esm/ImagePreview/ImagePreview.d.ts +53 -0
  37. package/dist/esm/ImagePreview/ImagePreview.js +42 -0
  38. package/dist/esm/ImagePreview/ImagePreview.test.d.ts +1 -0
  39. package/dist/esm/ImagePreview/ImagePreview.test.js +220 -0
  40. package/dist/esm/ImagePreview/index.d.ts +2 -0
  41. package/dist/esm/ImagePreview/index.js +2 -0
  42. package/dist/esm/Message/Message.d.ts +3 -0
  43. package/dist/esm/Message/Message.js +3 -2
  44. package/dist/esm/MessageBox/MessageBox.js +1 -1
  45. package/dist/esm/ToolCall/ToolCall.d.ts +44 -0
  46. package/dist/esm/ToolCall/ToolCall.js +10 -0
  47. package/dist/esm/ToolCall/ToolCall.test.d.ts +1 -0
  48. package/dist/esm/ToolCall/ToolCall.test.js +139 -0
  49. package/dist/esm/ToolCall/index.d.ts +2 -0
  50. package/dist/esm/ToolCall/index.js +2 -0
  51. package/dist/esm/index.d.ts +4 -0
  52. package/dist/esm/index.js +4 -0
  53. package/dist/tsconfig.tsbuildinfo +1 -1
  54. package/package.json +1 -1
  55. package/patternfly-docs/content/extensions/chatbot/examples/Messages/AttachmentEdit.tsx +1 -1
  56. package/patternfly-docs/content/extensions/chatbot/examples/Messages/ImagePreview.tsx +53 -0
  57. package/patternfly-docs/content/extensions/chatbot/examples/Messages/MessageWithToolCall.tsx +45 -0
  58. package/patternfly-docs/content/extensions/chatbot/examples/Messages/Messages.md +21 -1
  59. package/patternfly-docs/content/extensions/chatbot/examples/Messages/PreviewAttachment.tsx +1 -1
  60. package/patternfly-docs/content/extensions/chatbot/examples/Messages/file-preview.svg +9 -0
  61. package/patternfly-docs/content/extensions/chatbot/examples/demos/Chatbot.md +1 -1
  62. package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.scss +0 -12
  63. package/src/ChatbotConversationHistoryNav/ChatbotConversationHistoryNav.tsx +1 -1
  64. package/src/FileDetails/FileDetails.scss +10 -0
  65. package/src/FileDetails/FileDetails.test.tsx +16 -0
  66. package/src/FileDetails/FileDetails.tsx +89 -32
  67. package/src/FileDetails/__snapshots__/FileDetails.test.tsx.snap +25 -16
  68. package/src/FileDetailsLabel/FileDetailsLabel.test.tsx +21 -1
  69. package/src/FileDetailsLabel/FileDetailsLabel.tsx +16 -3
  70. package/src/FileDetailsLabel/__snapshots__/FileDetailsLabel.test.tsx.snap +25 -16
  71. package/src/ImagePreview/ImagePreview.scss +61 -0
  72. package/src/ImagePreview/ImagePreview.test.tsx +253 -0
  73. package/src/ImagePreview/ImagePreview.tsx +200 -0
  74. package/src/ImagePreview/index.ts +3 -0
  75. package/src/Message/Message.tsx +5 -0
  76. package/src/MessageBox/MessageBox.scss +0 -12
  77. package/src/MessageBox/MessageBox.tsx +1 -1
  78. package/src/ToolCall/ToolCall.scss +37 -0
  79. package/src/ToolCall/ToolCall.test.tsx +184 -0
  80. package/src/ToolCall/ToolCall.tsx +147 -0
  81. package/src/ToolCall/index.ts +3 -0
  82. package/src/index.ts +6 -0
  83. package/src/main.scss +14 -0
@@ -9,6 +9,7 @@ exports[`FileDetails should render file details 1`] = `
9
9
  class="pf-v6-l-flex pf-m-align-items-center pf-m-align-self-center pf-m-justify-content-center pf-chatbot__code-icon"
10
10
  >
11
11
  <svg
12
+ aria-hidden="true"
12
13
  fill="currentColor"
13
14
  height="24"
14
15
  viewBox="0 0 24 24"
@@ -40,30 +41,38 @@ exports[`FileDetails should render file details 1`] = `
40
41
  </svg>
41
42
  </div>
42
43
  <div
43
- class="pf-v6-l-stack"
44
+ class="pf-v6-l-flex pf-m-gap-xs"
44
45
  >
45
46
  <div
46
- class="pf-v6-l-stack__item"
47
+ class=""
47
48
  >
48
- <span
49
- class="pf-chatbot__code-fileName"
49
+ <div
50
+ class="pf-v6-l-flex pf-m-column pf-m-gap-none"
50
51
  >
51
- <span
52
- class="pf-v6-c-truncate"
53
- tabindex="0"
52
+ <div
53
+ class=""
54
54
  >
55
55
  <span
56
- class="pf-v6-c-truncate__start"
56
+ class="pf-chatbot__code-fileName"
57
57
  >
58
- test
58
+ <span
59
+ class="pf-v6-c-truncate"
60
+ tabindex="0"
61
+ >
62
+ <span
63
+ class="pf-v6-c-truncate__start"
64
+ >
65
+ test
66
+ </span>
67
+ </span>
59
68
  </span>
60
- </span>
61
- </span>
62
- </div>
63
- <div
64
- class="pf-v6-l-stack__item pf-chatbot__code-language"
65
- >
66
- TEXT
69
+ </div>
70
+ <div
71
+ class="pf-chatbot__code-language"
72
+ >
73
+ TEXT
74
+ </div>
75
+ </div>
67
76
  </div>
68
77
  </div>
69
78
  </div>
@@ -2,6 +2,7 @@ import { render, screen } from '@testing-library/react';
2
2
  import '@testing-library/jest-dom';
3
3
  import FileDetailsLabel from './FileDetailsLabel';
4
4
  import userEvent from '@testing-library/user-event';
5
+ import { BellIcon } from '@patternfly/react-icons';
5
6
 
6
7
  describe('FileDetailsLabel', () => {
7
8
  it('should render file details label', () => {
@@ -18,6 +19,19 @@ describe('FileDetailsLabel', () => {
18
19
  expect(screen.getByText('test')).toBeTruthy();
19
20
  expect(screen.queryByTestId('language')).toBeFalsy();
20
21
  });
22
+ it('should pass file size down', () => {
23
+ render(<FileDetailsLabel fileName="test.svg" fileSize="100MB" />);
24
+ expect(screen.getByText('100MB')).toBeTruthy();
25
+ });
26
+ it('should pass truncation prop down as true by default', () => {
27
+ render(<FileDetailsLabel fileName="test.svg" />);
28
+ expect(screen.getByText('test')).toBeTruthy();
29
+ expect(screen.queryByText('test.svg')).toBeFalsy();
30
+ });
31
+ it('should pass truncation prop down when false', () => {
32
+ render(<FileDetailsLabel fileName="test.svg" hasTruncation={false} />);
33
+ expect(screen.getByText('test.svg')).toBeTruthy();
34
+ });
21
35
  it('should not show spinner by default', () => {
22
36
  render(<FileDetailsLabel fileName="test.txt" spinnerTestId="spinner" />);
23
37
  expect(screen.queryByTestId('spinner')).toBeFalsy();
@@ -42,6 +56,12 @@ describe('FileDetailsLabel', () => {
42
56
  });
43
57
  it('should use closeButtonAriaLabel prop appropriately', () => {
44
58
  render(<FileDetailsLabel fileName="test.txt" onClose={jest.fn()} closeButtonAriaLabel="Delete file" />);
45
- screen.getByRole('button', { name: /Delete file/i });
59
+ expect(screen.getByRole('button', { name: /Delete file/i })).toBeTruthy();
60
+ });
61
+ it('should support custom close icon', () => {
62
+ render(
63
+ <FileDetailsLabel fileName="test.txt" onClose={jest.fn()} closeButtonIcon={<BellIcon data-testid="bell" />} />
64
+ );
65
+ expect(screen.getByTestId('bell')).toBeTruthy();
46
66
  });
47
67
  });
@@ -4,7 +4,7 @@ import FileDetails from '../FileDetails';
4
4
  import { Spinner } from '@patternfly/react-core';
5
5
  import { TimesIcon } from '@patternfly/react-icons';
6
6
 
7
- interface FileDetailsLabelProps {
7
+ export interface FileDetailsLabelProps {
8
8
  /** Name of file, including extension */
9
9
  fileName: string;
10
10
  /** Unique id of file */
@@ -21,6 +21,12 @@ interface FileDetailsLabelProps {
21
21
  languageTestId?: string;
22
22
  /** Custom test id for the loading spinner in the component */
23
23
  spinnerTestId?: string;
24
+ /** File size */
25
+ fileSize?: string;
26
+ /** Whether to truncate file name */
27
+ hasTruncation?: boolean;
28
+ /** Icon used for close button */
29
+ closeButtonIcon?: React.ReactNode;
24
30
  }
25
31
 
26
32
  export const FileDetailsLabel = ({
@@ -31,7 +37,11 @@ export const FileDetailsLabel = ({
31
37
  onClose,
32
38
  closeButtonAriaLabel,
33
39
  languageTestId,
34
- spinnerTestId
40
+ spinnerTestId,
41
+ fileSize,
42
+ hasTruncation = true,
43
+ closeButtonIcon = <TimesIcon />,
44
+ ...props
35
45
  }: PropsWithChildren<FileDetailsLabelProps>) => {
36
46
  const handleClose = (event) => {
37
47
  onClose && onClose(event, fileName, fileId);
@@ -45,17 +55,20 @@ export const FileDetailsLabel = ({
45
55
  type="button"
46
56
  variant="plain"
47
57
  aria-label={closeButtonAriaLabel ?? `Close ${fileName}`}
48
- icon={<TimesIcon />}
58
+ icon={closeButtonIcon}
49
59
  onClick={handleClose}
50
60
  />
51
61
  }
52
62
  {...(onClick && { onClick: (event) => onClick(event, fileName, fileId) })}
63
+ {...props}
53
64
  >
54
65
  <div className="pf-chatbot__file-label-contents">
55
66
  <FileDetails
56
67
  className={isLoading ? 'pf-chatbot__file-label-loading' : undefined}
57
68
  fileName={fileName}
58
69
  languageTestId={languageTestId}
70
+ fileSize={fileSize}
71
+ hasTruncation={hasTruncation}
59
72
  />
60
73
  {isLoading && <Spinner data-testid={spinnerTestId} size="sm" />}
61
74
  </div>
@@ -21,6 +21,7 @@ exports[`FileDetailsLabel should render file details label 1`] = `
21
21
  class="pf-v6-l-flex pf-m-align-items-center pf-m-align-self-center pf-m-justify-content-center pf-chatbot__code-icon"
22
22
  >
23
23
  <svg
24
+ aria-hidden="true"
24
25
  fill="currentColor"
25
26
  height="24"
26
27
  viewBox="0 0 24 24"
@@ -52,30 +53,38 @@ exports[`FileDetailsLabel should render file details label 1`] = `
52
53
  </svg>
53
54
  </div>
54
55
  <div
55
- class="pf-v6-l-stack"
56
+ class="pf-v6-l-flex pf-m-gap-xs"
56
57
  >
57
58
  <div
58
- class="pf-v6-l-stack__item"
59
+ class=""
59
60
  >
60
- <span
61
- class="pf-chatbot__code-fileName"
61
+ <div
62
+ class="pf-v6-l-flex pf-m-column pf-m-gap-none"
62
63
  >
63
- <span
64
- class="pf-v6-c-truncate"
65
- tabindex="0"
64
+ <div
65
+ class=""
66
66
  >
67
67
  <span
68
- class="pf-v6-c-truncate__start"
68
+ class="pf-chatbot__code-fileName"
69
69
  >
70
- test
70
+ <span
71
+ class="pf-v6-c-truncate"
72
+ tabindex="0"
73
+ >
74
+ <span
75
+ class="pf-v6-c-truncate__start"
76
+ >
77
+ test
78
+ </span>
79
+ </span>
71
80
  </span>
72
- </span>
73
- </span>
74
- </div>
75
- <div
76
- class="pf-v6-l-stack__item pf-chatbot__code-language"
77
- >
78
- TEXT
81
+ </div>
82
+ <div
83
+ class="pf-chatbot__code-language"
84
+ >
85
+ TEXT
86
+ </div>
87
+ </div>
79
88
  </div>
80
89
  </div>
81
90
  </div>
@@ -0,0 +1,61 @@
1
+ .pf-chatbot__image-preview-body {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--pf-t--global--spacer--lg);
5
+ --pf-v6-c-label--MaxWidth: initial;
6
+ --pf-v6-c-modal-box--ZIndex: var(--pf-t--global--z-index--2xl);
7
+
8
+ img {
9
+ flex: 1 0 0;
10
+ align-self: stretch;
11
+ }
12
+ .pf-chatbot__file-label {
13
+ min-width: fit-content;
14
+ }
15
+ }
16
+
17
+ .pf-chatbot__image-preview-stack {
18
+ height: unset;
19
+ }
20
+
21
+ .pf-v6-c-modal-box__footer.pf-chatbot__image-preview-footer {
22
+ padding-block-start: var(--pf-t--global--spacer--sm);
23
+ }
24
+
25
+ .pf-chatbot__image-preview-footer-buttons {
26
+ display: flex;
27
+ gap: var(--pf-t--global--spacer--xs);
28
+ align-items: center;
29
+ justify-content: space-between;
30
+ flex: 1;
31
+
32
+ .pf-v6-c-button {
33
+ border-radius: var(--pf-t--global--border--radius--pill);
34
+ padding: var(--pf-t--global--spacer--sm);
35
+ width: 2.31rem;
36
+ height: 2.31rem;
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+ }
41
+ button:disabled,
42
+ button[disabled] {
43
+ .pf-v6-c-icon__content {
44
+ color: var(--pf-t--global--icon--color--disabled);
45
+ }
46
+ }
47
+ .pf-v6-c-button__text {
48
+ display: flex;
49
+ align-items: center;
50
+ }
51
+ // Interactive states
52
+ .pf-v6-c-button:hover,
53
+ .pf-v6-c-button:focus {
54
+ .pf-v6-c-button__icon {
55
+ color: var(--pf-t--global--icon--color--regular);
56
+ }
57
+ }
58
+ .pf-v6-c-button__icon {
59
+ color: var(--pf-t--global--icon--color--subtle);
60
+ }
61
+ }
@@ -0,0 +1,253 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import ImagePreview from './ImagePreview';
5
+ import { ChatbotDisplayMode } from '../Chatbot';
6
+
7
+ const mockImages = [
8
+ {
9
+ fileName: 'image1.jpg',
10
+ fileSize: '2.5 MB',
11
+ image: <img src="" alt="Test image 1" />
12
+ },
13
+ {
14
+ fileName: 'image2.png',
15
+ fileSize: '1.8 MB',
16
+ image: <img src="" alt="Test image 2" />
17
+ },
18
+ {
19
+ fileName: 'image3.gif',
20
+ image: <img src="" alt="Test image 3" />
21
+ }
22
+ ];
23
+
24
+ const defaultProps = {
25
+ isModalOpen: true,
26
+ handleModalToggle: jest.fn(),
27
+ images: mockImages
28
+ };
29
+
30
+ describe('ImagePreview', () => {
31
+ beforeEach(() => {
32
+ jest.clearAllMocks();
33
+ });
34
+
35
+ it('renders modal when isModalOpen is true', () => {
36
+ render(<ImagePreview {...defaultProps} />);
37
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
38
+ });
39
+
40
+ it('does not render modal when isModalOpen is false', () => {
41
+ render(<ImagePreview {...defaultProps} isModalOpen={false} />);
42
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
43
+ });
44
+
45
+ it('displays custom title when provided', () => {
46
+ const customTitle = 'Custom image preview';
47
+ render(<ImagePreview {...defaultProps} title={customTitle} />);
48
+ expect(screen.getByRole('heading', { name: customTitle })).toBeInTheDocument();
49
+ });
50
+
51
+ it('displays default title when no title provided', () => {
52
+ render(<ImagePreview {...defaultProps} />);
53
+ expect(screen.getByRole('heading', { name: /Preview images/i })).toBeInTheDocument();
54
+ });
55
+
56
+ it('calls handleModalToggle when modal is closed', () => {
57
+ const mockHandleToggle = jest.fn();
58
+ render(<ImagePreview {...defaultProps} handleModalToggle={mockHandleToggle} />);
59
+ const closeButton = screen.getByRole('button', { name: /close/i });
60
+ fireEvent.click(closeButton);
61
+ expect(mockHandleToggle).toHaveBeenCalledTimes(1);
62
+ });
63
+
64
+ it('displays first image by default', () => {
65
+ render(<ImagePreview {...defaultProps} />);
66
+ expect(screen.getByText('image1.jpg')).toBeInTheDocument();
67
+ expect(screen.getByText('2.5 MB')).toBeInTheDocument();
68
+ expect(screen.getByAltText('Test image 1')).toBeInTheDocument();
69
+ });
70
+
71
+ it('displays page counter correctly', () => {
72
+ render(<ImagePreview {...defaultProps} />);
73
+ expect(screen.getByText('1/3')).toBeInTheDocument();
74
+ });
75
+
76
+ it('navigates to next image when next button is clicked', () => {
77
+ const mockOnNextClick = jest.fn();
78
+ render(<ImagePreview {...defaultProps} onNextClick={mockOnNextClick} />);
79
+ const nextButton = screen.getByRole('button', { name: /Go to next image/i });
80
+ fireEvent.click(nextButton);
81
+ expect(mockOnNextClick).toHaveBeenCalled();
82
+ expect(screen.getByText('2/3')).toBeInTheDocument();
83
+ expect(screen.getByText('image2.png')).toBeInTheDocument();
84
+ });
85
+
86
+ it('navigates to previous image when previous button is clicked', () => {
87
+ const mockOnPreviousClick = jest.fn();
88
+ render(<ImagePreview {...defaultProps} onPreviousClick={mockOnPreviousClick} />);
89
+ // First go to page 2
90
+ const nextButton = screen.getByRole('button', { name: /Go to next image/i });
91
+ fireEvent.click(nextButton);
92
+ // Then go back to page 1
93
+ const previousButton = screen.getByRole('button', { name: /Go to previous image/i });
94
+ fireEvent.click(previousButton);
95
+ expect(mockOnPreviousClick).toHaveBeenCalled();
96
+ expect(screen.getByText('1/3')).toBeInTheDocument();
97
+ });
98
+
99
+ it('calls onSetPage when page changes', () => {
100
+ const mockOnSetPage = jest.fn();
101
+ render(<ImagePreview {...defaultProps} onSetPage={mockOnSetPage} />);
102
+ const nextButton = screen.getByRole('button', { name: /Go to next image/i });
103
+ fireEvent.click(nextButton);
104
+ expect(mockOnSetPage).toHaveBeenCalledWith(expect.any(Object), 2);
105
+ });
106
+
107
+ it('disables previous button on first page', () => {
108
+ render(<ImagePreview {...defaultProps} />);
109
+ const previousButton = screen.getByRole('button', { name: /Go to previous image/i });
110
+ expect(previousButton).toBeDisabled();
111
+ });
112
+
113
+ it('disables next button on last page', () => {
114
+ render(<ImagePreview {...defaultProps} />);
115
+ // Navigate to last page
116
+ const nextButton = screen.getByRole('button', { name: /Go to next image/i });
117
+ fireEvent.click(nextButton); // page 2
118
+ fireEvent.click(nextButton); // page 3
119
+ expect(nextButton).toBeDisabled();
120
+ });
121
+
122
+ it('disables both navigation buttons when isDisabled is true', () => {
123
+ render(<ImagePreview {...defaultProps} isDisabled={true} />);
124
+ const previousButton = screen.getByRole('button', { name: /Go to previous image/i });
125
+ const nextButton = screen.getByRole('button', { name: /Go to next image/i });
126
+ expect(previousButton).toBeDisabled();
127
+ expect(nextButton).toBeDisabled();
128
+ });
129
+
130
+ it('uses custom aria labels for pagination', () => {
131
+ const customLabels = {
132
+ paginationAriaLabel: 'Custom pagination',
133
+ toPreviousPageAriaLabel: 'Go to previous image',
134
+ toNextPageAriaLabel: 'Go to next image'
135
+ };
136
+
137
+ render(<ImagePreview {...defaultProps} {...customLabels} />);
138
+ expect(screen.getByRole('navigation', { name: 'Custom pagination' })).toBeInTheDocument();
139
+ expect(screen.getByRole('button', { name: 'Go to previous image' })).toBeInTheDocument();
140
+ expect(screen.getByRole('button', { name: 'Go to next image' })).toBeInTheDocument();
141
+ });
142
+
143
+ it('renders with compact mode when isCompact is true', () => {
144
+ render(<ImagePreview {...defaultProps} isCompact={true} />);
145
+ const modal = screen.getByRole('dialog');
146
+ expect(modal).toHaveClass('pf-m-compact');
147
+ });
148
+
149
+ it('applies custom className when provided', () => {
150
+ const customClassName = 'custom-image-preview';
151
+ render(<ImagePreview {...defaultProps} className={customClassName} />);
152
+ const modal = screen.getByRole('dialog');
153
+ expect(modal).toHaveClass(customClassName);
154
+ });
155
+
156
+ it('applies display mode class correctly', () => {
157
+ render(<ImagePreview {...defaultProps} displayMode={ChatbotDisplayMode.embedded} />);
158
+ const modal = screen.getByRole('dialog');
159
+ expect(modal).toHaveClass('pf-chatbot__image-preview-modal--embedded');
160
+ });
161
+
162
+ it('passes additional props to ChatbotModal', () => {
163
+ const modalClass = 'custom-modal-class';
164
+ const additionalProps = {
165
+ 'data-testid': 'modal',
166
+ className: modalClass
167
+ };
168
+ render(<ImagePreview {...defaultProps} {...additionalProps} />);
169
+ const modal = screen.getByTestId('modal');
170
+ expect(modal).toBeInTheDocument();
171
+ expect(modal).toBeInTheDocument();
172
+ expect(modal).toHaveClass(modalClass);
173
+ });
174
+
175
+ it('passes modalHeaderProps correctly', () => {
176
+ const headerClass = 'custom-modal-header-class';
177
+ const headerProps = {
178
+ 'data-testid': 'header',
179
+ className: headerClass
180
+ };
181
+ render(<ImagePreview {...defaultProps} modalHeaderProps={headerProps} />);
182
+ expect(screen.getByTestId('header')).toBeInTheDocument();
183
+ expect(screen.getByTestId('header')).toHaveClass(headerClass);
184
+ });
185
+
186
+ it('passes modalBodyProps correctly', () => {
187
+ const bodyClass = 'custom-modal-body-class';
188
+ const bodyProps = {
189
+ 'data-testid': 'body',
190
+ className: bodyClass
191
+ };
192
+ render(<ImagePreview {...defaultProps} modalBodyProps={bodyProps} />);
193
+ expect(screen.getByTestId('body')).toBeInTheDocument();
194
+ expect(screen.getByTestId('body')).toHaveClass(bodyClass);
195
+ });
196
+
197
+ it('handles single image without pagination', () => {
198
+ const singleImage = [mockImages[0]];
199
+ render(<ImagePreview {...defaultProps} images={singleImage} />);
200
+ expect(screen.queryByText('1/1')).not.toBeInTheDocument();
201
+ expect(screen.queryByRole('button', { name: /Go to previous image/i })).not.toBeInTheDocument();
202
+ expect(screen.queryByRole('button', { name: /Go to next image/i })).not.toBeInTheDocument();
203
+ });
204
+
205
+ it('calls onCloseFileDetailsLabel when file details close button is clicked', () => {
206
+ const mockOnClose = jest.fn();
207
+ render(<ImagePreview {...defaultProps} onCloseFileDetailsLabel={mockOnClose} />);
208
+ const closeButton = screen.getByRole('button', { name: /Close image1.jpg/i });
209
+ fireEvent.click(closeButton);
210
+ expect(mockOnClose).toHaveBeenCalled();
211
+ });
212
+
213
+ it('passes fileDetailsLabelProps correctly to FileDetailsLabel', () => {
214
+ const customFileDetailsProps = {
215
+ 'data-testid': 'custom-file-details'
216
+ };
217
+ render(<ImagePreview {...defaultProps} fileDetailsLabelProps={customFileDetailsProps as any} />);
218
+ expect(screen.getByTestId('custom-file-details')).toBeInTheDocument();
219
+ });
220
+
221
+ it('displays file details for current page when navigating', () => {
222
+ render(<ImagePreview {...defaultProps} />);
223
+ // Initially shows first image details
224
+ expect(screen.getByText('image1.jpg')).toBeInTheDocument();
225
+ expect(screen.getByText('2.5 MB')).toBeInTheDocument();
226
+
227
+ // Navigate to second page
228
+ const nextButton = screen.getByRole('button', { name: /Go to next image/i });
229
+ fireEvent.click(nextButton);
230
+
231
+ // Should now show second image details
232
+ expect(screen.getByText('image2.png')).toBeInTheDocument();
233
+ expect(screen.getByText('1.8 MB')).toBeInTheDocument();
234
+
235
+ // Navigate to third page
236
+ fireEvent.click(nextButton);
237
+
238
+ // Should now show third image details (no file size)
239
+ expect(screen.getByText('image3.gif')).toBeInTheDocument();
240
+ expect(screen.queryByText(/MB/)).not.toBeInTheDocument();
241
+ });
242
+
243
+ it('sets hasTruncation to false on FileDetailsLabel', () => {
244
+ const longFileName = 'very-long-filename-that-would-normally-be-truncated-in-other-contexts.jpg';
245
+ const imageWithLongName = {
246
+ fileName: longFileName,
247
+ fileSize: '1.0 MB',
248
+ image: <img src="" alt="Test image with long name" />
249
+ };
250
+ render(<ImagePreview {...defaultProps} images={[imageWithLongName]} />);
251
+ expect(screen.getByText(longFileName)).toBeInTheDocument();
252
+ });
253
+ });