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,196 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import userEvent from '@testing-library/user-event';
4
+ import React from 'react';
5
+ import ToastContainer from '../../components/ToastContainer.jsx';
6
+
7
+ describe('ToastContainer', () => {
8
+ const defaultProps = {
9
+ toasts: [],
10
+ onRemove: vi.fn(),
11
+ };
12
+
13
+ beforeEach(() => {
14
+ vi.clearAllMocks();
15
+ });
16
+
17
+ describe('Rendering', () => {
18
+ it('renders toast container', () => {
19
+ render(<ToastContainer {...defaultProps} />);
20
+
21
+ const container = document.querySelector('.toast-container');
22
+ expect(container).toBeInTheDocument();
23
+ expect(container).toHaveAttribute('aria-live', 'polite');
24
+ });
25
+
26
+ it('renders nothing when toasts array is empty', () => {
27
+ render(<ToastContainer {...defaultProps} />);
28
+
29
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
30
+ });
31
+
32
+ it('renders success toast with correct icon', () => {
33
+ const toasts = [{ id: 1, message: 'Success message', type: 'success' }];
34
+ render(<ToastContainer {...defaultProps} toasts={toasts} />);
35
+
36
+ expect(screen.getByText('Success message')).toBeInTheDocument();
37
+ const toast = document.querySelector('.toast.success');
38
+ expect(toast).toBeInTheDocument();
39
+ });
40
+
41
+ it('renders error toast with correct icon', () => {
42
+ const toasts = [{ id: 1, message: 'Error message', type: 'error' }];
43
+ render(<ToastContainer {...defaultProps} toasts={toasts} />);
44
+
45
+ expect(screen.getByText('Error message')).toBeInTheDocument();
46
+ });
47
+
48
+ it('renders info toast with correct icon', () => {
49
+ const toasts = [{ id: 1, message: 'Info message', type: 'info' }];
50
+ render(<ToastContainer {...defaultProps} toasts={toasts} />);
51
+
52
+ expect(screen.getByText('Info message')).toBeInTheDocument();
53
+ });
54
+
55
+ it('renders multiple toasts', () => {
56
+ const toasts = [
57
+ { id: 1, message: 'First toast', type: 'success' },
58
+ { id: 2, message: 'Second toast', type: 'error' },
59
+ { id: 3, message: 'Third toast', type: 'info' },
60
+ ];
61
+ render(<ToastContainer {...defaultProps} toasts={toasts} />);
62
+
63
+ expect(screen.getByText('First toast')).toBeInTheDocument();
64
+ expect(screen.getByText('Second toast')).toBeInTheDocument();
65
+ expect(screen.getByText('Third toast')).toBeInTheDocument();
66
+ });
67
+
68
+ it('renders dismiss button for each toast', () => {
69
+ const toasts = [
70
+ { id: 1, message: 'Toast 1', type: 'success' },
71
+ { id: 2, message: 'Toast 2', type: 'error' },
72
+ ];
73
+ render(<ToastContainer {...defaultProps} toasts={toasts} />);
74
+
75
+ const dismissButtons = screen.getAllByLabelText('Dismiss');
76
+ expect(dismissButtons).toHaveLength(2);
77
+ });
78
+ });
79
+
80
+ describe('Interactions', () => {
81
+ it('calls onRemove with toast id when dismiss button is clicked', async () => {
82
+ const onRemove = vi.fn();
83
+ const toasts = [{ id: 123, message: 'Test toast', type: 'success' }];
84
+ render(<ToastContainer toasts={toasts} onRemove={onRemove} />);
85
+
86
+ const dismissButton = screen.getByLabelText('Dismiss');
87
+ await userEvent.click(dismissButton);
88
+
89
+ expect(onRemove).toHaveBeenCalledWith(123);
90
+ });
91
+
92
+ it('calls onRemove with correct id for each toast when dismissed', async () => {
93
+ const onRemove = vi.fn();
94
+ const toasts = [
95
+ { id: 1, message: 'Toast 1', type: 'success' },
96
+ { id: 2, message: 'Toast 2', type: 'error' },
97
+ { id: 3, message: 'Toast 3', type: 'info' },
98
+ ];
99
+ render(<ToastContainer toasts={toasts} onRemove={onRemove} />);
100
+
101
+ const dismissButtons = screen.getAllByLabelText('Dismiss');
102
+
103
+ await userEvent.click(dismissButtons[0]);
104
+ expect(onRemove).toHaveBeenCalledWith(1);
105
+
106
+ await userEvent.click(dismissButtons[1]);
107
+ expect(onRemove).toHaveBeenCalledWith(2);
108
+
109
+ await userEvent.click(dismissButtons[2]);
110
+ expect(onRemove).toHaveBeenCalledWith(3);
111
+ });
112
+ });
113
+
114
+ describe('Accessibility', () => {
115
+ it('container has aria-live polite attribute', () => {
116
+ render(<ToastContainer {...defaultProps} />);
117
+
118
+ const container = document.querySelector('.toast-container');
119
+ expect(container).toHaveAttribute('aria-live', 'polite');
120
+ });
121
+
122
+ it('each toast has role alert', () => {
123
+ const toasts = [{ id: 1, message: 'Test alert', type: 'success' }];
124
+ render(<ToastContainer {...defaultProps} toasts={toasts} />);
125
+
126
+ const toast = document.querySelector('.toast');
127
+ expect(toast).toHaveAttribute('role', 'alert');
128
+ });
129
+
130
+ it('dismiss buttons have aria-label', () => {
131
+ const toasts = [{ id: 1, message: 'Test toast', type: 'success' }];
132
+ render(<ToastContainer {...defaultProps} toasts={toasts} />);
133
+
134
+ expect(screen.getByLabelText('Dismiss')).toBeInTheDocument();
135
+ });
136
+ });
137
+
138
+ describe('Toast Types', () => {
139
+ it('renders success toast with CheckCircle icon', () => {
140
+ const toasts = [{ id: 1, message: 'Success', type: 'success' }];
141
+ render(<ToastContainer {...defaultProps} toasts={toasts} />);
142
+
143
+ const toast = screen.getByRole('alert');
144
+ expect(toast).toHaveClass('success');
145
+ });
146
+
147
+ it('renders error toast with AlertCircle icon', () => {
148
+ const toasts = [{ id: 1, message: 'Error', type: 'error' }];
149
+ render(<ToastContainer {...defaultProps} toasts={toasts} />);
150
+
151
+ const toast = screen.getByRole('alert');
152
+ expect(toast).toHaveClass('error');
153
+ });
154
+
155
+ it('renders info toast with Info icon', () => {
156
+ const toasts = [{ id: 1, message: 'Info', type: 'info' }];
157
+ render(<ToastContainer {...defaultProps} toasts={toasts} />);
158
+
159
+ const toast = screen.getByRole('alert');
160
+ expect(toast).toHaveClass('info');
161
+ });
162
+
163
+ it('handles unknown toast type gracefully', () => {
164
+ const toasts = [{ id: 1, message: 'Unknown', type: 'unknown' }];
165
+ render(<ToastContainer {...defaultProps} toasts={toasts} />);
166
+
167
+ const toast = screen.getByRole('alert');
168
+ expect(toast).toHaveClass('unknown');
169
+ });
170
+ });
171
+
172
+ describe('Edge Cases', () => {
173
+ it('handles empty message gracefully', () => {
174
+ const toasts = [{ id: 1, message: '', type: 'info' }];
175
+ render(<ToastContainer {...defaultProps} toasts={toasts} />);
176
+
177
+ expect(screen.getByRole('alert')).toBeInTheDocument();
178
+ });
179
+
180
+ it('handles long message text', () => {
181
+ const longMessage = 'This is a very long toast message that should still render correctly without breaking the layout';
182
+ const toasts = [{ id: 1, message: longMessage, type: 'success' }];
183
+ render(<ToastContainer {...defaultProps} toasts={toasts} />);
184
+
185
+ expect(screen.getByText(longMessage)).toBeInTheDocument();
186
+ });
187
+
188
+ it('handles special characters in message', () => {
189
+ const specialMessage = 'Test <script>alert("xss")</script> & more';
190
+ const toasts = [{ id: 1, message: specialMessage, type: 'info' }];
191
+ render(<ToastContainer {...defaultProps} toasts={toasts} />);
192
+
193
+ expect(screen.getByText(specialMessage)).toBeInTheDocument();
194
+ });
195
+ });
196
+ });
@@ -0,0 +1,197 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { renderHook, act } from '@testing-library/react';
3
+ import { useFocusTrap } from '../../hooks/useFocusTrap';
4
+
5
+ describe('useFocusTrap', () => {
6
+ let container;
7
+ let button1;
8
+ let button2;
9
+ let input;
10
+ let previousActiveElement;
11
+
12
+ beforeEach(() => {
13
+ previousActiveElement = document.createElement('button');
14
+ previousActiveElement.textContent = 'Previous';
15
+ document.body.appendChild(previousActiveElement);
16
+ previousActiveElement.focus();
17
+
18
+ container = document.createElement('div');
19
+ button1 = document.createElement('button');
20
+ button1.textContent = 'First Button';
21
+ button1.tabIndex = 0;
22
+
23
+ button2 = document.createElement('button');
24
+ button2.textContent = 'Second Button';
25
+ button2.tabIndex = 0;
26
+
27
+ input = document.createElement('input');
28
+ input.type = 'text';
29
+ input.tabIndex = 0;
30
+
31
+ container.appendChild(button1);
32
+ container.appendChild(input);
33
+ container.appendChild(button2);
34
+ document.body.appendChild(container);
35
+ });
36
+
37
+ afterEach(() => {
38
+ if (container && container.parentNode) {
39
+ container.parentNode.removeChild(container);
40
+ }
41
+ if (previousActiveElement && previousActiveElement.parentNode) {
42
+ previousActiveElement.parentNode.removeChild(previousActiveElement);
43
+ }
44
+ vi.restoreAllMocks();
45
+ });
46
+
47
+ it('should not trap focus when isActive is false', () => {
48
+ const ref = { current: container };
49
+
50
+ renderHook(() => useFocusTrap(ref, false));
51
+
52
+ expect(document.activeElement).not.toBe(button1);
53
+ });
54
+
55
+ it('should trap focus when isActive becomes true', () => {
56
+ const ref = { current: container };
57
+
58
+ const { rerender } = renderHook(
59
+ ({ active }) => useFocusTrap({ current: container }, active),
60
+ { initialProps: { active: false } }
61
+ );
62
+
63
+ rerender({ active: true });
64
+
65
+ expect(document.activeElement).toBe(button1);
66
+ });
67
+
68
+ it('should focus first focusable element when activated', () => {
69
+ const ref = { current: container };
70
+
71
+ renderHook(() => useFocusTrap(ref, true));
72
+
73
+ expect(document.activeElement).toBe(button1);
74
+ });
75
+
76
+ it('should return focus to previous element on cleanup', () => {
77
+ const ref = { current: container };
78
+
79
+ const { unmount } = renderHook(() => useFocusTrap(ref, true));
80
+
81
+ expect(document.activeElement).toBe(button1);
82
+
83
+ unmount();
84
+
85
+ expect(document.activeElement).toBe(previousActiveElement);
86
+ });
87
+
88
+ it('should cycle focus forward from last to first element', () => {
89
+ const simpleContainer = document.createElement('div');
90
+ const btnA = document.createElement('button');
91
+ btnA.textContent = 'A';
92
+ const btnB = document.createElement('button');
93
+ btnB.textContent = 'B';
94
+ simpleContainer.appendChild(btnA);
95
+ simpleContainer.appendChild(btnB);
96
+ document.body.appendChild(simpleContainer);
97
+
98
+ const ref = { current: simpleContainer };
99
+ renderHook(() => useFocusTrap(ref, true));
100
+
101
+ btnB.focus();
102
+ expect(document.activeElement).toBe(btnB);
103
+
104
+ const tabEvent = new KeyboardEvent('keydown', {
105
+ key: 'Tab',
106
+ bubbles: true,
107
+ cancelable: true
108
+ });
109
+ Object.defineProperty(tabEvent, 'shiftKey', { value: false });
110
+ simpleContainer.dispatchEvent(tabEvent);
111
+
112
+ expect(document.activeElement).toBe(btnA);
113
+ simpleContainer.parentNode.removeChild(simpleContainer);
114
+ });
115
+
116
+ it('should cycle focus backward from first to last element', () => {
117
+ const simpleContainer = document.createElement('div');
118
+ const btnA = document.createElement('button');
119
+ btnA.textContent = 'A';
120
+ const btnB = document.createElement('button');
121
+ btnB.textContent = 'B';
122
+ simpleContainer.appendChild(btnA);
123
+ simpleContainer.appendChild(btnB);
124
+ document.body.appendChild(simpleContainer);
125
+
126
+ const ref = { current: simpleContainer };
127
+ renderHook(() => useFocusTrap(ref, true));
128
+
129
+ btnA.focus();
130
+ expect(document.activeElement).toBe(btnA);
131
+
132
+ const shiftTabEvent = new KeyboardEvent('keydown', {
133
+ key: 'Tab',
134
+ bubbles: true,
135
+ cancelable: true
136
+ });
137
+ Object.defineProperty(shiftTabEvent, 'shiftKey', { value: true });
138
+ simpleContainer.dispatchEvent(shiftTabEvent);
139
+
140
+ expect(document.activeElement).toBe(btnB);
141
+ simpleContainer.parentNode.removeChild(simpleContainer);
142
+ });
143
+
144
+ it('should not trap non-Tab keys', () => {
145
+ const ref = { current: container };
146
+
147
+ renderHook(() => useFocusTrap(ref, true));
148
+
149
+ button1.focus();
150
+ expect(document.activeElement).toBe(button1);
151
+
152
+ const enterEvent = new KeyboardEvent('keydown', {
153
+ key: 'Enter',
154
+ bubbles: true
155
+ });
156
+ container.dispatchEvent(enterEvent);
157
+
158
+ expect(document.activeElement).toBe(button1);
159
+ });
160
+
161
+ it('should handle empty container gracefully', () => {
162
+ const emptyContainer = document.createElement('div');
163
+ document.body.appendChild(emptyContainer);
164
+
165
+ const { unmount } = renderHook(() => useFocusTrap({ current: emptyContainer }, true));
166
+
167
+ expect(document.activeElement).not.toBe(emptyContainer);
168
+
169
+ unmount();
170
+ emptyContainer.parentNode.removeChild(emptyContainer);
171
+ });
172
+
173
+ it('should ignore Tab key when no focusable elements exist', () => {
174
+ const emptyContainer = document.createElement('div');
175
+ document.body.appendChild(emptyContainer);
176
+
177
+ renderHook(() => useFocusTrap({ current: emptyContainer }, true));
178
+
179
+ const tabEvent = new KeyboardEvent('keydown', {
180
+ key: 'Tab',
181
+ bubbles: true,
182
+ cancelable: true
183
+ });
184
+ emptyContainer.dispatchEvent(tabEvent);
185
+
186
+ expect(tabEvent.defaultPrevented).toBe(false);
187
+ emptyContainer.parentNode.removeChild(emptyContainer);
188
+ });
189
+
190
+ it('should not include elements with tabindex="-1" in trap', () => {
191
+ const ref = { current: container };
192
+
193
+ renderHook(() => useFocusTrap(ref, true));
194
+
195
+ expect(document.activeElement).toBe(button1);
196
+ });
197
+ });