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,375 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { render, screen, waitFor, fireEvent } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import React from 'react';
5
+ import CreateIssueModal from '../../components/CreateIssueModal.jsx';
6
+
7
+ const mockProjects = [
8
+ { id: '10001', key: 'PROJ', name: 'Project 1' },
9
+ { id: '10002', key: 'TEST', name: 'Test Project' },
10
+ ];
11
+
12
+ const mockIssueTypes = [
13
+ { id: '10010', name: 'Bug', subtask: false },
14
+ { id: '10011', name: 'Story', subtask: false },
15
+ { id: '10012', name: 'Subtask', subtask: true },
16
+ ];
17
+
18
+ const mockFetchIssueTypes = vi.fn().mockResolvedValue(mockIssueTypes);
19
+ const mockCreateIssue = vi.fn().mockResolvedValue({ key: 'PROJ-123' });
20
+
21
+ vi.mock('../../services/api', () => ({
22
+ fetchIssueTypes: (...args) => mockFetchIssueTypes(...args),
23
+ createIssue: (...args) => mockCreateIssue(...args),
24
+ }));
25
+
26
+ describe('CreateIssueModal', () => {
27
+ const defaultProps = {
28
+ isOpen: true,
29
+ onClose: vi.fn(),
30
+ onCreated: vi.fn(),
31
+ projects: mockProjects,
32
+ projectsLoading: false,
33
+ addToast: vi.fn(),
34
+ };
35
+
36
+ beforeEach(() => {
37
+ vi.clearAllMocks();
38
+ mockFetchIssueTypes.mockResolvedValue(mockIssueTypes);
39
+ mockCreateIssue.mockResolvedValue({ key: 'PROJ-123' });
40
+ });
41
+
42
+ describe('Rendering', () => {
43
+ it('renders modal when isOpen is true', () => {
44
+ render(<CreateIssueModal {...defaultProps} />);
45
+
46
+ expect(screen.getByRole('dialog')).toBeInTheDocument();
47
+ expect(screen.getByText('Create Issue')).toBeInTheDocument();
48
+ });
49
+
50
+ it('returns null when isOpen is false', () => {
51
+ render(<CreateIssueModal {...defaultProps} isOpen={false} />);
52
+
53
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
54
+ });
55
+
56
+ it('renders project dropdown with options', () => {
57
+ render(<CreateIssueModal {...defaultProps} />);
58
+
59
+ const projectSelect = screen.getByLabelText('Select project');
60
+ expect(projectSelect).toBeInTheDocument();
61
+ expect(screen.getByText('Project 1 (PROJ)')).toBeInTheDocument();
62
+ expect(screen.getByText('Test Project (TEST)')).toBeInTheDocument();
63
+ });
64
+
65
+ it('renders summary input field', () => {
66
+ render(<CreateIssueModal {...defaultProps} />);
67
+
68
+ const summaryInput = screen.getByLabelText('Issue summary');
69
+ expect(summaryInput).toBeInTheDocument();
70
+ });
71
+
72
+ it('renders description textarea', () => {
73
+ render(<CreateIssueModal {...defaultProps} />);
74
+
75
+ const descTextarea = screen.getByLabelText('Issue description');
76
+ expect(descTextarea).toBeInTheDocument();
77
+ });
78
+
79
+ it('renders priority dropdown', () => {
80
+ render(<CreateIssueModal {...defaultProps} />);
81
+
82
+ const prioritySelect = screen.getByLabelText('Select priority');
83
+ expect(prioritySelect).toBeInTheDocument();
84
+ expect(screen.getByText('Highest')).toBeInTheDocument();
85
+ expect(screen.getByText('Lowest')).toBeInTheDocument();
86
+ });
87
+
88
+ it('renders Cancel and Create buttons', () => {
89
+ render(<CreateIssueModal {...defaultProps} />);
90
+
91
+ expect(screen.getByText('Cancel')).toBeInTheDocument();
92
+ expect(screen.getByText('Create')).toBeInTheDocument();
93
+ });
94
+
95
+ it('shows loading text in project dropdown when loading', () => {
96
+ render(<CreateIssueModal {...defaultProps} projectsLoading={true} />);
97
+
98
+ expect(screen.getByText('Loading...')).toBeInTheDocument();
99
+ });
100
+ });
101
+
102
+ describe('Form Interactions', () => {
103
+ it('shows issue type dropdown after selecting a project', async () => {
104
+ render(<CreateIssueModal {...defaultProps} />);
105
+
106
+ const projectSelect = screen.getByLabelText('Select project');
107
+ await userEvent.selectOptions(projectSelect, 'PROJ');
108
+
109
+ await waitFor(() => {
110
+ expect(screen.getByLabelText('Select issue type')).toBeInTheDocument();
111
+ });
112
+
113
+ expect(screen.getByText('Bug')).toBeInTheDocument();
114
+ expect(screen.getByText('Story')).toBeInTheDocument();
115
+ });
116
+
117
+ it('filters out subtask issue types', async () => {
118
+ render(<CreateIssueModal {...defaultProps} />);
119
+
120
+ const projectSelect = screen.getByLabelText('Select project');
121
+ await userEvent.selectOptions(projectSelect, 'PROJ');
122
+
123
+ await waitFor(() => {
124
+ expect(screen.queryByText('Subtask')).not.toBeInTheDocument();
125
+ });
126
+ });
127
+
128
+ it('calls onClose when Cancel is clicked', async () => {
129
+ render(<CreateIssueModal {...defaultProps} />);
130
+
131
+ await userEvent.click(screen.getByText('Cancel'));
132
+
133
+ expect(defaultProps.onClose).toHaveBeenCalled();
134
+ });
135
+
136
+ it('calls setCreateData when typing in description', async () => {
137
+ render(<CreateIssueModal {...defaultProps} />);
138
+
139
+ const descTextarea = screen.getByLabelText('Issue description');
140
+ await userEvent.type(descTextarea, 'Test description content');
141
+
142
+ expect(descTextarea.value).toBe('Test description content');
143
+ });
144
+
145
+ it('resets issueType when project changes', async () => {
146
+ render(<CreateIssueModal {...defaultProps} />);
147
+
148
+ const projectSelect = screen.getByLabelText('Select project');
149
+ await userEvent.selectOptions(projectSelect, 'PROJ');
150
+
151
+ await waitFor(() => {
152
+ expect(screen.getByLabelText('Select issue type')).toBeInTheDocument();
153
+ });
154
+
155
+ const issueTypeSelect = screen.getByLabelText('Select issue type');
156
+ await userEvent.selectOptions(issueTypeSelect, '10010');
157
+
158
+ await waitFor(() => {
159
+ expect(issueTypeSelect.value).toBe('10010');
160
+ });
161
+
162
+ await userEvent.selectOptions(projectSelect, 'TEST');
163
+
164
+ await waitFor(() => {
165
+ expect(issueTypeSelect.value).toBe('');
166
+ });
167
+ });
168
+ });
169
+
170
+ describe('Form Validation', () => {
171
+ it('disables Create button when form is incomplete', () => {
172
+ render(<CreateIssueModal {...defaultProps} />);
173
+
174
+ const createButton = screen.getByLabelText('Create issue');
175
+ expect(createButton).toBeDisabled();
176
+ });
177
+
178
+ it('enables Create button when all required fields are filled', async () => {
179
+ render(<CreateIssueModal {...defaultProps} />);
180
+
181
+ const projectSelect = screen.getByLabelText('Select project');
182
+ await userEvent.selectOptions(projectSelect, 'PROJ');
183
+
184
+ await waitFor(() => {
185
+ const issueTypeSelect = screen.getByLabelText('Select issue type');
186
+ userEvent.selectOptions(issueTypeSelect, '10010');
187
+ });
188
+
189
+ await userEvent.type(screen.getByLabelText('Issue summary'), 'Test Issue');
190
+
191
+ await waitFor(() => {
192
+ const createButton = screen.getByLabelText('Create issue');
193
+ expect(createButton).not.toBeDisabled();
194
+ });
195
+ });
196
+
197
+ it('disables Create button when project is missing', () => {
198
+ render(<CreateIssueModal {...defaultProps} />);
199
+
200
+ const summaryInput = screen.getByLabelText('Issue summary');
201
+ userEvent.type(summaryInput, 'Test Issue');
202
+
203
+ const createButton = screen.getByLabelText('Create issue');
204
+ expect(createButton).toBeDisabled();
205
+ });
206
+
207
+ it('does not call createIssue when submit is triggered with missing fields', async () => {
208
+ render(<CreateIssueModal {...defaultProps} />);
209
+
210
+ const summaryInput = screen.getByLabelText('Issue summary');
211
+ await userEvent.type(summaryInput, 'Test Issue');
212
+
213
+ const createButton = screen.getByLabelText('Create issue');
214
+ fireEvent.click(createButton);
215
+
216
+ expect(mockCreateIssue).not.toHaveBeenCalled();
217
+ });
218
+
219
+ it('disables Create button when summary is missing', async () => {
220
+ render(<CreateIssueModal {...defaultProps} />);
221
+
222
+ const projectSelect = screen.getByLabelText('Select project');
223
+ await userEvent.selectOptions(projectSelect, 'PROJ');
224
+
225
+ await waitFor(() => {
226
+ const issueTypeSelect = screen.getByLabelText('Select issue type');
227
+ userEvent.selectOptions(issueTypeSelect, '10010');
228
+ });
229
+
230
+ const createButton = screen.getByLabelText('Create issue');
231
+ expect(createButton).toBeDisabled();
232
+ });
233
+
234
+ it('changes priority when priority dropdown is selected', async () => {
235
+ render(<CreateIssueModal {...defaultProps} />);
236
+
237
+ const prioritySelect = screen.getByLabelText('Select priority');
238
+ await userEvent.selectOptions(prioritySelect, '2');
239
+
240
+ expect(prioritySelect.value).toBe('2');
241
+ });
242
+ });
243
+
244
+ describe('Submit', () => {
245
+ it('calls createIssue and callbacks on successful submission', async () => {
246
+ render(<CreateIssueModal {...defaultProps} />);
247
+
248
+ const projectSelect = screen.getByLabelText('Select project');
249
+ await userEvent.selectOptions(projectSelect, 'PROJ');
250
+
251
+ await waitFor(() => {
252
+ const issueTypeSelect = screen.getByLabelText('Select issue type');
253
+ userEvent.selectOptions(issueTypeSelect, '10010');
254
+ });
255
+
256
+ await userEvent.type(screen.getByLabelText('Issue summary'), 'Test Issue');
257
+
258
+ await waitFor(() => {
259
+ const createButton = screen.getByLabelText('Create issue');
260
+ userEvent.click(createButton);
261
+ });
262
+
263
+ await waitFor(() => {
264
+ expect(mockCreateIssue).toHaveBeenCalled();
265
+ expect(defaultProps.onCreated).toHaveBeenCalled();
266
+ expect(defaultProps.addToast).toHaveBeenCalledWith('Issue created successfully', 'success');
267
+ });
268
+ });
269
+
270
+ it('shows loading state while creating', async () => {
271
+ mockCreateIssue.mockImplementation(() => new Promise(resolve => setTimeout(resolve, 100)));
272
+
273
+ render(<CreateIssueModal {...defaultProps} />);
274
+
275
+ const projectSelect = screen.getByLabelText('Select project');
276
+ await userEvent.selectOptions(projectSelect, 'PROJ');
277
+
278
+ await waitFor(() => {
279
+ const issueTypeSelect = screen.getByLabelText('Select issue type');
280
+ userEvent.selectOptions(issueTypeSelect, '10010');
281
+ });
282
+
283
+ await userEvent.type(screen.getByLabelText('Issue summary'), 'Test Issue');
284
+
285
+ await waitFor(() => {
286
+ const createButton = screen.getByLabelText('Create issue');
287
+ userEvent.click(createButton);
288
+ });
289
+
290
+ await waitFor(() => {
291
+ expect(screen.getByText('Creating...')).toBeInTheDocument();
292
+ });
293
+ });
294
+
295
+ it('displays error message on failed creation', async () => {
296
+ mockCreateIssue.mockRejectedValueOnce(new Error('Failed to create'));
297
+
298
+ render(<CreateIssueModal {...defaultProps} />);
299
+
300
+ const projectSelect = screen.getByLabelText('Select project');
301
+ await userEvent.selectOptions(projectSelect, 'PROJ');
302
+
303
+ await waitFor(() => {
304
+ const issueTypeSelect = screen.getByLabelText('Select issue type');
305
+ userEvent.selectOptions(issueTypeSelect, '10010');
306
+ });
307
+
308
+ await userEvent.type(screen.getByLabelText('Issue summary'), 'Test Issue');
309
+
310
+ await waitFor(() => {
311
+ const createButton = screen.getByLabelText('Create issue');
312
+ userEvent.click(createButton);
313
+ });
314
+
315
+ await waitFor(() => {
316
+ expect(screen.getByText('Failed to create')).toBeInTheDocument();
317
+ expect(defaultProps.addToast).toHaveBeenCalledWith(
318
+ expect.stringContaining('Error creating issue'),
319
+ 'error'
320
+ );
321
+ });
322
+ });
323
+
324
+ it('displays fallback error message when error has no message', async () => {
325
+ mockCreateIssue.mockRejectedValueOnce(new Error());
326
+
327
+ render(<CreateIssueModal {...defaultProps} />);
328
+
329
+ const projectSelect = screen.getByLabelText('Select project');
330
+ await userEvent.selectOptions(projectSelect, 'PROJ');
331
+
332
+ await waitFor(() => {
333
+ const issueTypeSelect = screen.getByLabelText('Select issue type');
334
+ userEvent.selectOptions(issueTypeSelect, '10010');
335
+ });
336
+
337
+ await userEvent.type(screen.getByLabelText('Issue summary'), 'Test Issue');
338
+
339
+ await waitFor(() => {
340
+ const createButton = screen.getByLabelText('Create issue');
341
+ userEvent.click(createButton);
342
+ });
343
+
344
+ await waitFor(() => {
345
+ expect(screen.getByText('Failed to create issue')).toBeInTheDocument();
346
+ });
347
+ });
348
+ });
349
+
350
+ describe('Accessibility', () => {
351
+ it('modal has correct aria attributes', () => {
352
+ render(<CreateIssueModal {...defaultProps} />);
353
+
354
+ const modal = screen.getByRole('dialog');
355
+ expect(modal).toHaveAttribute('aria-modal', 'true');
356
+ expect(modal).toHaveAttribute('aria-labelledby', 'modal-title');
357
+ });
358
+
359
+ it('close button has aria-label', () => {
360
+ render(<CreateIssueModal {...defaultProps} />);
361
+
362
+ const closeButton = screen.getByLabelText('Close modal');
363
+ expect(closeButton).toBeInTheDocument();
364
+ });
365
+
366
+ it('form inputs have aria-labels', () => {
367
+ render(<CreateIssueModal {...defaultProps} />);
368
+
369
+ expect(screen.getByLabelText('Select project')).toBeInTheDocument();
370
+ expect(screen.getByLabelText('Issue summary')).toBeInTheDocument();
371
+ expect(screen.getByLabelText('Issue description')).toBeInTheDocument();
372
+ expect(screen.getByLabelText('Select priority')).toBeInTheDocument();
373
+ });
374
+ });
375
+ });