jira-pat 1.0.0

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 (95) hide show
  1. package/AGENTS.md +218 -0
  2. package/README.md +64 -0
  3. package/backend/.env.example +1 -0
  4. package/backend/__tests__/getJiraClient.test.js +57 -0
  5. package/backend/__tests__/issues.test.js +565 -0
  6. package/backend/__tests__/jiraService.test.js +1127 -0
  7. package/backend/__tests__/projects.test.js +256 -0
  8. package/backend/coverage/clover.xml +426 -0
  9. package/backend/coverage/coverage-final.json +4 -0
  10. package/backend/coverage/lcov-report/base.css +224 -0
  11. package/backend/coverage/lcov-report/block-navigation.js +87 -0
  12. package/backend/coverage/lcov-report/favicon.png +0 -0
  13. package/backend/coverage/lcov-report/index.html +131 -0
  14. package/backend/coverage/lcov-report/prettify.css +1 -0
  15. package/backend/coverage/lcov-report/prettify.js +2 -0
  16. package/backend/coverage/lcov-report/routes/index.html +131 -0
  17. package/backend/coverage/lcov-report/routes/issues.js.html +823 -0
  18. package/backend/coverage/lcov-report/routes/projects.js.html +190 -0
  19. package/backend/coverage/lcov-report/service/index.html +116 -0
  20. package/backend/coverage/lcov-report/service/jiraService.js.html +1663 -0
  21. package/backend/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  22. package/backend/coverage/lcov-report/sorter.js +210 -0
  23. package/backend/coverage/lcov.info +707 -0
  24. package/backend/index.js +38 -0
  25. package/backend/jest.config.js +11 -0
  26. package/backend/package-lock.json +5636 -0
  27. package/backend/package.json +28 -0
  28. package/backend/routes/issues.js +246 -0
  29. package/backend/routes/projects.js +35 -0
  30. package/backend/service/jiraService.js +526 -0
  31. package/bin/jira.js +92 -0
  32. package/frontend/.env.example +1 -0
  33. package/frontend/coverage/base.css +224 -0
  34. package/frontend/coverage/block-navigation.js +87 -0
  35. package/frontend/coverage/clover.xml +559 -0
  36. package/frontend/coverage/components/CreateIssueModal.jsx.html +592 -0
  37. package/frontend/coverage/components/IssueDetailPanel.jsx.html +1633 -0
  38. package/frontend/coverage/components/IssueDrawer.jsx.html +550 -0
  39. package/frontend/coverage/components/IssueTable.jsx.html +571 -0
  40. package/frontend/coverage/components/SkeletonComponents.jsx.html +223 -0
  41. package/frontend/coverage/components/ToastContainer.jsx.html +142 -0
  42. package/frontend/coverage/components/index.html +191 -0
  43. package/frontend/coverage/coverage-final.json +14 -0
  44. package/frontend/coverage/favicon.png +0 -0
  45. package/frontend/coverage/hooks/index.html +161 -0
  46. package/frontend/coverage/hooks/useFocusTrap.js.html +262 -0
  47. package/frontend/coverage/hooks/useIssueDrawer.js.html +1000 -0
  48. package/frontend/coverage/hooks/useIssuesList.js.html +175 -0
  49. package/frontend/coverage/hooks/useToasts.js.html +142 -0
  50. package/frontend/coverage/index.html +161 -0
  51. package/frontend/coverage/prettify.css +1 -0
  52. package/frontend/coverage/prettify.js +2 -0
  53. package/frontend/coverage/services/api.js.html +547 -0
  54. package/frontend/coverage/services/index.html +116 -0
  55. package/frontend/coverage/sort-arrow-sprite.png +0 -0
  56. package/frontend/coverage/sorter.js +210 -0
  57. package/frontend/coverage/utils/index.html +131 -0
  58. package/frontend/coverage/utils/issueHelpers.jsx.html +334 -0
  59. package/frontend/coverage/utils/sanitize.js.html +166 -0
  60. package/frontend/index.html +13 -0
  61. package/frontend/package-lock.json +3436 -0
  62. package/frontend/package.json +30 -0
  63. package/frontend/src/App.jsx +447 -0
  64. package/frontend/src/__tests__/components/CreateIssueModal.test.jsx +375 -0
  65. package/frontend/src/__tests__/components/IssueDetailPanel.test.jsx +962 -0
  66. package/frontend/src/__tests__/components/IssueDrawer.test.jsx +240 -0
  67. package/frontend/src/__tests__/components/IssueTable.test.jsx +423 -0
  68. package/frontend/src/__tests__/components/ToastContainer.test.jsx +196 -0
  69. package/frontend/src/__tests__/hooks/useFocusTrap.test.js +197 -0
  70. package/frontend/src/__tests__/hooks/useIssueDrawer.test.js +1053 -0
  71. package/frontend/src/__tests__/hooks/useIssuesList.test.js +175 -0
  72. package/frontend/src/__tests__/hooks/useToasts.test.js +110 -0
  73. package/frontend/src/__tests__/services/api.test.js +568 -0
  74. package/frontend/src/__tests__/setup.js +54 -0
  75. package/frontend/src/__tests__/utils/issueHelpers.test.jsx +336 -0
  76. package/frontend/src/__tests__/utils/sanitize.test.js +238 -0
  77. package/frontend/src/components/CreateIssueModal.jsx +169 -0
  78. package/frontend/src/components/ErrorBoundary.jsx +52 -0
  79. package/frontend/src/components/IssueDetailPanel.jsx +517 -0
  80. package/frontend/src/components/IssueDrawer.jsx +155 -0
  81. package/frontend/src/components/IssueTable.jsx +162 -0
  82. package/frontend/src/components/SkeletonComponents.jsx +46 -0
  83. package/frontend/src/components/StandaloneIssuePage.jsx +176 -0
  84. package/frontend/src/components/ToastContainer.jsx +19 -0
  85. package/frontend/src/hooks/useFocusTrap.js +59 -0
  86. package/frontend/src/hooks/useIssueDrawer.js +305 -0
  87. package/frontend/src/hooks/useIssuesList.js +30 -0
  88. package/frontend/src/hooks/useToasts.js +19 -0
  89. package/frontend/src/index.css +2070 -0
  90. package/frontend/src/main.jsx +13 -0
  91. package/frontend/src/services/api.js +154 -0
  92. package/frontend/src/utils/issueHelpers.jsx +84 -0
  93. package/frontend/src/utils/sanitize.js +27 -0
  94. package/frontend/vite.config.js +15 -0
  95. package/package.json +19 -0
@@ -0,0 +1,336 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ getIssueTypeIcon,
4
+ getStatusClass,
5
+ getPriorityIcon,
6
+ getSeverityIcon,
7
+ formatSeverity,
8
+ getInitials,
9
+ SEVERITY_FIELD
10
+ } from '../../utils/issueHelpers';
11
+
12
+ describe('issueHelpers', () => {
13
+ describe('SEVERITY_FIELD', () => {
14
+ it('should export the correct severity field name', () => {
15
+ expect(SEVERITY_FIELD).toBe('customfield_10260');
16
+ });
17
+ });
18
+
19
+ describe('getIssueTypeIcon', () => {
20
+ it('should return a React element for null/undefined input', () => {
21
+ const resultNull = getIssueTypeIcon(null);
22
+ const resultUndefined = getIssueTypeIcon(undefined);
23
+
24
+ expect(resultNull).toBeTruthy();
25
+ expect(resultNull.type).toBeDefined();
26
+ expect(resultUndefined).toBeTruthy();
27
+ expect(resultUndefined.type).toBeDefined();
28
+ });
29
+
30
+ it('should return a Bug icon element for types containing "bug"', () => {
31
+ const result = getIssueTypeIcon('Bug');
32
+ expect(result).toBeTruthy();
33
+ expect(result.type).toBeDefined();
34
+ expect(result.props.size).toBe(16);
35
+ });
36
+
37
+ it('should return a Layers icon element for story types', () => {
38
+ const result = getIssueTypeIcon('Story');
39
+ expect(result).toBeTruthy();
40
+ expect(result.type).toBeDefined();
41
+ expect(result.props.size).toBe(16);
42
+ });
43
+
44
+ it('should return a Layers icon element for epic types', () => {
45
+ const result = getIssueTypeIcon('Epic');
46
+ expect(result).toBeTruthy();
47
+ expect(result.type).toBeDefined();
48
+ expect(result.props.size).toBe(16);
49
+ });
50
+
51
+ it('should return a ClipboardList icon element for task types', () => {
52
+ const result = getIssueTypeIcon('Task');
53
+ expect(result).toBeTruthy();
54
+ expect(result.type).toBeDefined();
55
+ expect(result.props.size).toBe(16);
56
+ });
57
+
58
+ it('should return a FileText icon element as default for unknown types', () => {
59
+ const result = getIssueTypeIcon('Improvement');
60
+ expect(result).toBeTruthy();
61
+ expect(result.type).toBeDefined();
62
+ expect(result.props.size).toBe(16);
63
+ });
64
+
65
+ it('should handle case-insensitive matching', () => {
66
+ const bugResult = getIssueTypeIcon('BUG');
67
+ const storyResult = getIssueTypeIcon('STORY');
68
+ const taskResult = getIssueTypeIcon('TASK');
69
+
70
+ expect(bugResult).toBeTruthy();
71
+ expect(storyResult).toBeTruthy();
72
+ expect(taskResult).toBeTruthy();
73
+ });
74
+
75
+ it('should handle mixed case types', () => {
76
+ expect(getIssueTypeIcon('Bug Report')).toBeTruthy();
77
+ expect(getIssueTypeIcon('user story')).toBeTruthy();
78
+ });
79
+ });
80
+
81
+ describe('getStatusClass', () => {
82
+ it('should return empty string for null/undefined input', () => {
83
+ expect(getStatusClass(null)).toBe('');
84
+ expect(getStatusClass(undefined)).toBe('');
85
+ });
86
+
87
+ it('should return "in-progress" for in-progress status', () => {
88
+ expect(getStatusClass('in-progress')).toBe('in-progress');
89
+ });
90
+
91
+ it('should return "done" for done status', () => {
92
+ expect(getStatusClass('done')).toBe('done');
93
+ });
94
+
95
+ it('should return "closed" for closed status', () => {
96
+ expect(getStatusClass('closed')).toBe('closed');
97
+ });
98
+
99
+ it('should return "todo" as default for unknown statuses', () => {
100
+ expect(getStatusClass('To Do')).toBe('todo');
101
+ expect(getStatusClass('Open')).toBe('todo');
102
+ expect(getStatusClass('Backlog')).toBe('todo');
103
+ });
104
+
105
+ it('should normalize status by replacing spaces with hyphens', () => {
106
+ expect(getStatusClass('in progress')).toBe('in-progress');
107
+ expect(getStatusClass('IN PROGRESS')).toBe('in-progress');
108
+ });
109
+
110
+ it('should handle mixed case input', () => {
111
+ expect(getStatusClass('In-Progress')).toBe('in-progress');
112
+ expect(getStatusClass('DONE')).toBe('done');
113
+ });
114
+ });
115
+
116
+ describe('getPriorityIcon', () => {
117
+ it('should return null for null/undefined input', () => {
118
+ expect(getPriorityIcon(null)).toBeNull();
119
+ expect(getPriorityIcon(undefined)).toBeNull();
120
+ });
121
+
122
+ it('should return ChevronUp for highest priority', () => {
123
+ const result = getPriorityIcon('Highest');
124
+ expect(result).toBeTruthy();
125
+ expect(result.type).toBeDefined();
126
+ expect(result.props.size).toBe(16);
127
+ });
128
+
129
+ it('should return ChevronUp for high priority', () => {
130
+ const result = getPriorityIcon('High');
131
+ expect(result).toBeTruthy();
132
+ expect(result.type).toBeDefined();
133
+ expect(result.props.size).toBe(16);
134
+ });
135
+
136
+ it('should return ChevronDown for lowest priority', () => {
137
+ const result = getPriorityIcon('Lowest');
138
+ expect(result).toBeTruthy();
139
+ expect(result.type).toBeDefined();
140
+ expect(result.props.size).toBe(16);
141
+ });
142
+
143
+ it('should return ChevronDown for low priority', () => {
144
+ const result = getPriorityIcon('Low');
145
+ expect(result).toBeTruthy();
146
+ expect(result.type).toBeDefined();
147
+ expect(result.props.size).toBe(16);
148
+ });
149
+
150
+ it('should return Minus for medium priority (default)', () => {
151
+ const result = getPriorityIcon('Medium');
152
+ expect(result).toBeTruthy();
153
+ expect(result.type).toBeDefined();
154
+ expect(result.props.size).toBe(16);
155
+ });
156
+
157
+ it('should handle case-insensitive matching', () => {
158
+ expect(getPriorityIcon('HIGHEST')).toBeTruthy();
159
+ expect(getPriorityIcon('LOW')).toBeTruthy();
160
+ expect(getPriorityIcon('MEDIUM')).toBeTruthy();
161
+ });
162
+ });
163
+
164
+ describe('getSeverityIcon', () => {
165
+ it('should return null for null/undefined input', () => {
166
+ expect(getSeverityIcon(null)).toBeNull();
167
+ expect(getSeverityIcon(undefined)).toBeNull();
168
+ });
169
+
170
+ it('should return AlertCircle for blocker severity', () => {
171
+ const result = getSeverityIcon('Blocker');
172
+ expect(result).toBeTruthy();
173
+ expect(result.type).toBeDefined();
174
+ expect(result.props.size).toBe(14);
175
+ });
176
+
177
+ it('should return AlertCircle for critical severity', () => {
178
+ const result = getSeverityIcon('Critical');
179
+ expect(result).toBeTruthy();
180
+ expect(result.type).toBeDefined();
181
+ expect(result.props.size).toBe(14);
182
+ });
183
+
184
+ it('should return AlertCircle for sev-1 severity', () => {
185
+ const result = getSeverityIcon('Sev-1');
186
+ expect(result).toBeTruthy();
187
+ expect(result.type).toBeDefined();
188
+ expect(result.props.size).toBe(14);
189
+ });
190
+
191
+ it('should return ChevronUp for major severity', () => {
192
+ const result = getSeverityIcon('Major');
193
+ expect(result).toBeTruthy();
194
+ expect(result.type).toBeDefined();
195
+ expect(result.props.size).toBe(14);
196
+ });
197
+
198
+ it('should return ChevronUp for sev-2 severity', () => {
199
+ const result = getSeverityIcon('Sev-2');
200
+ expect(result).toBeTruthy();
201
+ expect(result.type).toBeDefined();
202
+ expect(result.props.size).toBe(14);
203
+ });
204
+
205
+ it('should return Minus for minor severity', () => {
206
+ const result = getSeverityIcon('Minor');
207
+ expect(result).toBeTruthy();
208
+ expect(result.type).toBeDefined();
209
+ expect(result.props.size).toBe(14);
210
+ });
211
+
212
+ it('should return Minus for sev-3 severity', () => {
213
+ const result = getSeverityIcon('Sev-3');
214
+ expect(result).toBeTruthy();
215
+ expect(result.type).toBeDefined();
216
+ expect(result.props.size).toBe(14);
217
+ });
218
+
219
+ it('should return ChevronDown for trivial severity', () => {
220
+ const result = getSeverityIcon('Trivial');
221
+ expect(result).toBeTruthy();
222
+ expect(result.type).toBeDefined();
223
+ expect(result.props.size).toBe(14);
224
+ });
225
+
226
+ it('should return ChevronDown for sev-4 severity', () => {
227
+ const result = getSeverityIcon('Sev-4');
228
+ expect(result).toBeTruthy();
229
+ expect(result.type).toBeDefined();
230
+ expect(result.props.size).toBe(14);
231
+ });
232
+
233
+ it('should return ChevronDown for sev-5 severity', () => {
234
+ const result = getSeverityIcon('Sev-5');
235
+ expect(result).toBeTruthy();
236
+ expect(result.type).toBeDefined();
237
+ expect(result.props.size).toBe(14);
238
+ });
239
+
240
+ it('should return Info for unknown severity (default)', () => {
241
+ const result = getSeverityIcon('Low');
242
+ expect(result).toBeTruthy();
243
+ expect(result.type).toBeDefined();
244
+ expect(result.props.size).toBe(14);
245
+ });
246
+
247
+ it('should handle case-insensitive matching', () => {
248
+ expect(getSeverityIcon('BLOCKER')).toBeTruthy();
249
+ expect(getSeverityIcon('MAJOR')).toBeTruthy();
250
+ expect(getSeverityIcon('SEV-1')).toBeTruthy();
251
+ });
252
+ });
253
+
254
+ describe('formatSeverity', () => {
255
+ it('should return empty string for null/undefined input', () => {
256
+ expect(formatSeverity(null)).toBe('');
257
+ expect(formatSeverity(undefined)).toBe('');
258
+ });
259
+
260
+ it('should return the severity string as-is', () => {
261
+ expect(formatSeverity('Critical')).toBe('Critical');
262
+ expect(formatSeverity('Minor')).toBe('Minor');
263
+ expect(formatSeverity('Low')).toBe('Low');
264
+ });
265
+
266
+ it('should handle numeric severity values', () => {
267
+ expect(formatSeverity(1)).toBe(1);
268
+ expect(formatSeverity(5)).toBe(5);
269
+ });
270
+ });
271
+
272
+ describe('getInitials', () => {
273
+ it('should return "?" for null input', () => {
274
+ expect(getInitials(null)).toBe('?');
275
+ });
276
+
277
+ it('should return "?" for undefined input', () => {
278
+ expect(getInitials(undefined)).toBe('?');
279
+ });
280
+
281
+ it('should return "?" for non-string input', () => {
282
+ expect(getInitials(123)).toBe('?');
283
+ expect(getInitials({})).toBe('?');
284
+ expect(getInitials([])).toBe('?');
285
+ });
286
+
287
+ it('should return "?" for empty string', () => {
288
+ expect(getInitials('')).toBe('?');
289
+ });
290
+
291
+ it('should return "?" for whitespace-only string', () => {
292
+ expect(getInitials(' ')).toBe('?');
293
+ });
294
+
295
+ it('should return "@" for string that is just @ (atIndex is 0, not > 0)', () => {
296
+ expect(getInitials('@')).toBe('@');
297
+ });
298
+
299
+ it('should return first two characters for single word name', () => {
300
+ expect(getInitials('John')).toBe('JO');
301
+ expect(getInitials('alice')).toBe('AL');
302
+ });
303
+
304
+ it('should return first and last initials for full name', () => {
305
+ expect(getInitials('John Doe')).toBe('JD');
306
+ expect(getInitials('Alice Smith')).toBe('AS');
307
+ });
308
+
309
+ it('should handle names with multiple spaces', () => {
310
+ expect(getInitials('John Doe')).toBe('JD');
311
+ });
312
+
313
+ it('should handle email addresses by extracting name part before @', () => {
314
+ expect(getInitials('john.doe@example.com')).toBe('JO');
315
+ expect(getInitials('user@domain.org')).toBe('US');
316
+ });
317
+
318
+ it('should handle email with single name', () => {
319
+ expect(getInitials('admin@example.com')).toBe('AD');
320
+ });
321
+
322
+ it('should handle names with special characters', () => {
323
+ expect(getInitials("John O'Brien")).toBe('JO');
324
+ expect(getInitials('Mary-Jane Watson')).toBe('MW');
325
+ });
326
+
327
+ it('should return uppercase initials', () => {
328
+ expect(getInitials('john doe')).toBe('JD');
329
+ expect(getInitials('ALICE SMITH')).toBe('AS');
330
+ });
331
+
332
+ it('should handle string that splits to only whitespace parts', () => {
333
+ expect(getInitials(' \t \t ')).toBe('?');
334
+ });
335
+ });
336
+ });
@@ -0,0 +1,238 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { sanitizeHtml } from '../../utils/sanitize';
3
+
4
+ describe('sanitize', () => {
5
+ describe('sanitizeHtml', () => {
6
+ it('should return empty string for null input', () => {
7
+ expect(sanitizeHtml(null)).toBe('');
8
+ });
9
+
10
+ it('should return empty string for undefined input', () => {
11
+ expect(sanitizeHtml(undefined)).toBe('');
12
+ });
13
+
14
+ it('should return empty string for empty string input', () => {
15
+ expect(sanitizeHtml('')).toBe('');
16
+ });
17
+
18
+ it('should preserve plain text', () => {
19
+ expect(sanitizeHtml('Hello World')).toBe('Hello World');
20
+ });
21
+
22
+ it('should preserve allowed tags: p', () => {
23
+ const result = sanitizeHtml('<p>Paragraph</p>');
24
+ expect(result).toContain('<p>Paragraph</p>');
25
+ });
26
+
27
+ it('should preserve allowed tags: strong', () => {
28
+ const result = sanitizeHtml('<strong>Bold text</strong>');
29
+ expect(result).toContain('<strong>Bold text</strong>');
30
+ });
31
+
32
+ it('should preserve allowed tags: em', () => {
33
+ const result = sanitizeHtml('<em>Italic text</em>');
34
+ expect(result).toContain('<em>Italic text</em>');
35
+ });
36
+
37
+ it('should preserve allowed tags: u', () => {
38
+ const result = sanitizeHtml('<u>Underlined text</u>');
39
+ expect(result).toContain('<u>Underlined text</u>');
40
+ });
41
+
42
+ it('should preserve allowed tags: s (strikethrough)', () => {
43
+ const result = sanitizeHtml('<s>Strikethrough</s>');
44
+ expect(result).toContain('<s>Strikethrough</s>');
45
+ });
46
+
47
+ it('should preserve allowed tags: del', () => {
48
+ const result = sanitizeHtml('<del>Deleted text</del>');
49
+ expect(result).toContain('<del>Deleted text</del>');
50
+ });
51
+
52
+ it('should preserve allowed tags: ins', () => {
53
+ const result = sanitizeHtml('<ins>Inserted text</ins>');
54
+ expect(result).toContain('<ins>Inserted text</ins>');
55
+ });
56
+
57
+ it('should preserve allowed tags: a with href', () => {
58
+ const result = sanitizeHtml('<a href="https://example.com">Link</a>');
59
+ expect(result).toContain('<a href="https://example.com">Link</a>');
60
+ });
61
+
62
+ it('should preserve allowed tags: img with src and alt', () => {
63
+ const result = sanitizeHtml('<img src="image.png" alt="Description" />');
64
+ expect(result).toContain('<img src="image.png" alt="Description"');
65
+ });
66
+
67
+ it('should preserve allowed tags: span', () => {
68
+ const result = sanitizeHtml('<span>Span text</span>');
69
+ expect(result).toContain('<span>Span text</span>');
70
+ });
71
+
72
+ it('should preserve allowed tags: div', () => {
73
+ const result = sanitizeHtml('<div>Div content</div>');
74
+ expect(result).toContain('<div>Div content</div>');
75
+ });
76
+
77
+ it('should preserve allowed tags: ul, ol, li', () => {
78
+ const result = sanitizeHtml('<ul><li>Item 1</li><li>Item 2</li></ul>');
79
+ expect(result).toContain('<ul>');
80
+ expect(result).toContain('<li>Item 1</li>');
81
+ expect(result).toContain('<li>Item 2</li>');
82
+ });
83
+
84
+ it('should preserve allowed tags: h1-h6', () => {
85
+ expect(sanitizeHtml('<h1>Heading 1</h1>')).toContain('<h1>Heading 1</h1>');
86
+ expect(sanitizeHtml('<h2>Heading 2</h2>')).toContain('<h2>Heading 2</h2>');
87
+ expect(sanitizeHtml('<h3>Heading 3</h3>')).toContain('<h3>Heading 3</h3>');
88
+ expect(sanitizeHtml('<h4>Heading 4</h4>')).toContain('<h4>Heading 4</h4>');
89
+ expect(sanitizeHtml('<h5>Heading 5</h5>')).toContain('<h5>Heading 5</h5>');
90
+ expect(sanitizeHtml('<h6>Heading 6</h6>')).toContain('<h6>Heading 6</h6>');
91
+ });
92
+
93
+ it('should preserve allowed tags: code', () => {
94
+ const result = sanitizeHtml('<code>inline code</code>');
95
+ expect(result).toContain('<code>inline code</code>');
96
+ });
97
+
98
+ it('should preserve allowed tags: pre', () => {
99
+ const result = sanitizeHtml('<pre>Preformatted text</pre>');
100
+ expect(result).toContain('<pre>Preformatted text</pre>');
101
+ });
102
+
103
+ it('should preserve allowed tags: blockquote', () => {
104
+ const result = sanitizeHtml('<blockquote>Quote text</blockquote>');
105
+ expect(result).toContain('<blockquote>Quote text</blockquote>');
106
+ });
107
+
108
+ it('should preserve allowed tags: table elements', () => {
109
+ const html = '<table><thead><tbody><tr><th>Header</th></tr><tr><td>Data</td></tr></tbody></thead></table>';
110
+ const result = sanitizeHtml(html);
111
+ expect(result).toContain('<table>');
112
+ expect(result).toContain('<thead>');
113
+ expect(result).toContain('<tbody>');
114
+ expect(result).toContain('<tr>');
115
+ expect(result).toContain('<th>Header</th>');
116
+ expect(result).toContain('<td>Data</td>');
117
+ });
118
+
119
+ it('should preserve allowed tags: br', () => {
120
+ const result = sanitizeHtml('Line 1<br>Line 2');
121
+ expect(result).toContain('<br>');
122
+ });
123
+
124
+ it('should preserve allowed tags: hr', () => {
125
+ const result = sanitizeHtml('Before<hr>After');
126
+ expect(result).toContain('<hr>');
127
+ });
128
+
129
+ it('should preserve allowed tags: sup and sub', () => {
130
+ expect(sanitizeHtml('x<sup>2</sup>')).toContain('<sup>2</sup>');
131
+ expect(sanitizeHtml('H<sub>2</sub>O')).toContain('<sub>2</sub>');
132
+ });
133
+
134
+ it('should preserve allowed attributes: class', () => {
135
+ const result = sanitizeHtml('<div class="my-class">Content</div>');
136
+ expect(result).toContain('class="my-class"');
137
+ });
138
+
139
+ it('should preserve allowed attributes: id', () => {
140
+ const result = sanitizeHtml('<div id="my-id">Content</div>');
141
+ expect(result).toContain('id="my-id"');
142
+ });
143
+
144
+ it('should preserve allowed attributes: style', () => {
145
+ const result = sanitizeHtml('<div style="color: red">Content</div>');
146
+ expect(result).toContain('style="color: red"');
147
+ });
148
+
149
+ it('should preserve allowed attributes: target', () => {
150
+ const result = sanitizeHtml('<a href="url" target="_blank">Link</a>');
151
+ expect(result).toContain('target="_blank"');
152
+ });
153
+
154
+ it('should preserve allowed attributes: colspan and rowspan', () => {
155
+ const result = sanitizeHtml('<td colspan="2" rowspan="3">Cell</td>');
156
+ expect(result).toContain('Cell');
157
+ });
158
+
159
+ it('should remove script tags', () => {
160
+ const result = sanitizeHtml('<p>Safe</p><script>alert("xss")</script>');
161
+ expect(result).not.toContain('<script>');
162
+ expect(result).toContain('<p>Safe</p>');
163
+ });
164
+
165
+ it('should remove onclick handlers', () => {
166
+ const result = sanitizeHtml('<div onclick="alert(1)">Click me</div>');
167
+ expect(result).not.toContain('onclick');
168
+ expect(result).toContain('<div>Click me</div>');
169
+ });
170
+
171
+ it('should remove onerror handlers', () => {
172
+ const result = sanitizeHtml('<img src="x" onerror="alert(1)" />');
173
+ expect(result).not.toContain('onerror');
174
+ });
175
+
176
+ it('should remove javascript: URLs', () => {
177
+ const result = sanitizeHtml('<a href="javascript:alert(1)">Evil</a>');
178
+ expect(result).not.toContain('javascript:');
179
+ });
180
+
181
+ it('should remove iframe tags', () => {
182
+ const result = sanitizeHtml('<p>Text</p><iframe src="evil.com"></iframe>');
183
+ expect(result).not.toContain('<iframe>');
184
+ expect(result).toContain('<p>Text</p>');
185
+ });
186
+
187
+ it('should remove object tags', () => {
188
+ const result = sanitizeHtml('<p>Text</p><object data="evil.swf"></object>');
189
+ expect(result).not.toContain('<object>');
190
+ expect(result).toContain('<p>Text</p>');
191
+ });
192
+
193
+ it('should remove embed tags', () => {
194
+ const result = sanitizeHtml('<p>Text</p><embed src="evil.swf">');
195
+ expect(result).not.toContain('<embed>');
196
+ expect(result).toContain('<p>Text</p>');
197
+ });
198
+
199
+ it('should remove form tags', () => {
200
+ const result = sanitizeHtml('<p>Text</p><form action="evil.com"><input name="q"></form>');
201
+ expect(result).not.toContain('<form>');
202
+ expect(result).not.toContain('<input>');
203
+ });
204
+
205
+ it('should handle mixed content with safe and unsafe HTML', () => {
206
+ const result = sanitizeHtml('<p>Hello <strong>world</strong></p><script>evil()</script>');
207
+ expect(result).toContain('<p>Hello <strong>world</strong></p>');
208
+ expect(result).not.toContain('<script>');
209
+ });
210
+
211
+ it('should preserve nested allowed tags', () => {
212
+ const result = sanitizeHtml('<div><p><strong><em>Nested</em></strong></p></div>');
213
+ expect(result).toContain('<div>');
214
+ expect(result).toContain('<p>');
215
+ expect(result).toContain('<strong>');
216
+ expect(result).toContain('<em>Nested</em>');
217
+ });
218
+
219
+ it('should handle complex HTML structure', () => {
220
+ const html = `
221
+ <h1>Title</h1>
222
+ <p>Paragraph with <a href="https://example.com">link</a></p>
223
+ <ul>
224
+ <li>Item 1</li>
225
+ <li>Item 2</li>
226
+ </ul>
227
+ <pre><code>code block</code></pre>
228
+ `;
229
+ const result = sanitizeHtml(html);
230
+ expect(result).toContain('<h1>Title</h1>');
231
+ expect(result).toContain('<p>Paragraph with <a href="https://example.com">link</a></p>');
232
+ expect(result).toContain('<ul>');
233
+ expect(result).toContain('<li>Item 1</li>');
234
+ expect(result).toContain('<li>Item 2</li>');
235
+ expect(result).toContain('<pre><code>code block</code></pre>');
236
+ });
237
+ });
238
+ });