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,175 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { renderHook, waitFor } from '@testing-library/react';
3
+ import { useIssuesList } from '../../hooks/useIssuesList';
4
+ import * as api from '../../services/api';
5
+
6
+ vi.mock('../../services/api', () => ({
7
+ getIssues: vi.fn()
8
+ }));
9
+
10
+ describe('useIssuesList', () => {
11
+ beforeEach(() => {
12
+ vi.clearAllMocks();
13
+ });
14
+
15
+ it('should initialize with empty issues, loading true, and null error', () => {
16
+ api.getIssues.mockImplementation(() => new Promise(() => {}));
17
+
18
+ const { result } = renderHook(() => useIssuesList('TEST', null));
19
+
20
+ expect(result.current.issues).toEqual([]);
21
+ expect(result.current.loading).toBe(true);
22
+ expect(result.current.error).toBe(null);
23
+ });
24
+
25
+ it('should fetch issues on mount', async () => {
26
+ const mockIssues = [
27
+ { key: 'TEST-1', fields: { summary: 'Issue 1' } },
28
+ { key: 'TEST-2', fields: { summary: 'Issue 2' } }
29
+ ];
30
+ api.getIssues.mockResolvedValue({ issues: mockIssues });
31
+
32
+ const { result } = renderHook(() => useIssuesList('TEST', null));
33
+
34
+ await waitFor(() => {
35
+ expect(result.current.loading).toBe(false);
36
+ });
37
+
38
+ expect(result.current.issues).toEqual(mockIssues);
39
+ expect(api.getIssues).toHaveBeenCalledWith({
40
+ project: 'TEST',
41
+ status: 'exclude:Done,Cancelled,Closed'
42
+ });
43
+ });
44
+
45
+ it('should apply status filter when provided', async () => {
46
+ api.getIssues.mockResolvedValue({ issues: [] });
47
+
48
+ renderHook(() => useIssuesList('TEST', 'In Progress'));
49
+
50
+ await waitFor(() => {
51
+ expect(api.getIssues).toHaveBeenCalledWith({
52
+ project: 'TEST',
53
+ status: 'In Progress'
54
+ });
55
+ });
56
+ });
57
+
58
+ it('should handle API errors', async () => {
59
+ const errorMessage = 'Failed to fetch issues';
60
+ api.getIssues.mockRejectedValue(new Error(errorMessage));
61
+
62
+ const { result } = renderHook(() => useIssuesList('TEST', null));
63
+
64
+ await waitFor(() => {
65
+ expect(result.current.loading).toBe(false);
66
+ });
67
+
68
+ expect(result.current.error).toBe(errorMessage);
69
+ expect(result.current.issues).toEqual([]);
70
+ });
71
+
72
+ it('should use fallback error message when error has no message', async () => {
73
+ api.getIssues.mockRejectedValue(new Error());
74
+
75
+ const { result } = renderHook(() => useIssuesList('TEST', null));
76
+
77
+ await waitFor(() => {
78
+ expect(result.current.loading).toBe(false);
79
+ });
80
+
81
+ expect(result.current.error).toBe('Failed to fetch issues');
82
+ });
83
+
84
+ it('should refetch when projectKey changes', async () => {
85
+ api.getIssues.mockResolvedValue({ issues: [{ key: 'TEST-1' }] });
86
+
87
+ const { rerender } = renderHook(
88
+ ({ project }) => useIssuesList(project, null),
89
+ { initialProps: { project: 'PROJ1' } }
90
+ );
91
+
92
+ await waitFor(() => {
93
+ expect(api.getIssues).toHaveBeenCalled();
94
+ });
95
+
96
+ api.getIssues.mockClear();
97
+ api.getIssues.mockResolvedValue({ issues: [{ key: 'PROJ2-1' }] });
98
+
99
+ rerender({ project: 'PROJ2' });
100
+
101
+ await waitFor(() => {
102
+ expect(api.getIssues).toHaveBeenCalledWith({
103
+ project: 'PROJ2',
104
+ status: 'exclude:Done,Cancelled,Closed'
105
+ });
106
+ });
107
+ });
108
+
109
+ it('should refetch when statusFilter changes', async () => {
110
+ api.getIssues.mockResolvedValue({ issues: [] });
111
+
112
+ const { rerender } = renderHook(
113
+ ({ project, status }) => useIssuesList(project, status),
114
+ { initialProps: { project: 'TEST', status: null } }
115
+ );
116
+
117
+ await waitFor(() => {
118
+ expect(result => result.current.loading === false);
119
+ });
120
+
121
+ api.getIssues.mockClear();
122
+
123
+ rerender({ project: 'TEST', status: 'In Review' });
124
+
125
+ await waitFor(() => {
126
+ expect(api.getIssues).toHaveBeenCalledWith({
127
+ project: 'TEST',
128
+ status: 'In Review'
129
+ });
130
+ });
131
+ });
132
+
133
+ it('should provide refetch function', async () => {
134
+ api.getIssues
135
+ .mockResolvedValueOnce({ issues: [{ key: 'TEST-1' }] })
136
+ .mockResolvedValueOnce({ issues: [{ key: 'TEST-2' }] });
137
+
138
+ const { result, rerender } = renderHook(() => useIssuesList('TEST', null));
139
+
140
+ await waitFor(() => {
141
+ expect(result.current.loading).toBe(false);
142
+ });
143
+
144
+ expect(result.current.issues[0].key).toBe('TEST-1');
145
+
146
+ await result.current.refetch();
147
+ rerender();
148
+
149
+ expect(result.current.issues[0].key).toBe('TEST-2');
150
+ });
151
+
152
+ it('should handle empty issues response', async () => {
153
+ api.getIssues.mockResolvedValue({ issues: [] });
154
+
155
+ const { result } = renderHook(() => useIssuesList('TEST', null));
156
+
157
+ await waitFor(() => {
158
+ expect(result.current.loading).toBe(false);
159
+ });
160
+
161
+ expect(result.current.issues).toEqual([]);
162
+ });
163
+
164
+ it('should handle undefined issues in response', async () => {
165
+ api.getIssues.mockResolvedValue({});
166
+
167
+ const { result } = renderHook(() => useIssuesList('TEST', null));
168
+
169
+ await waitFor(() => {
170
+ expect(result.current.loading).toBe(false);
171
+ });
172
+
173
+ expect(result.current.issues).toEqual([]);
174
+ });
175
+ });
@@ -0,0 +1,110 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { renderHook, act } from '@testing-library/react';
3
+ import { useToasts } from '../../hooks/useToasts';
4
+
5
+ describe('useToasts', () => {
6
+ beforeEach(() => {
7
+ vi.useFakeTimers();
8
+ });
9
+
10
+ afterEach(() => {
11
+ vi.useRealTimers();
12
+ vi.clearAllMocks();
13
+ });
14
+
15
+ it('should initialize with empty toasts array', () => {
16
+ const { result } = renderHook(() => useToasts());
17
+ expect(result.current.toasts).toEqual([]);
18
+ });
19
+
20
+ it('should add a toast with default type info', () => {
21
+ const { result } = renderHook(() => useToasts());
22
+
23
+ act(() => {
24
+ result.current.addToast('Test message');
25
+ });
26
+
27
+ expect(result.current.toasts).toHaveLength(1);
28
+ expect(result.current.toasts[0]).toMatchObject({
29
+ message: 'Test message',
30
+ type: 'info'
31
+ });
32
+ });
33
+
34
+ it('should add a toast with custom type', () => {
35
+ const { result } = renderHook(() => useToasts());
36
+
37
+ act(() => {
38
+ result.current.addToast('Success message', 'success');
39
+ });
40
+
41
+ expect(result.current.toasts[0]).toMatchObject({
42
+ message: 'Success message',
43
+ type: 'success'
44
+ });
45
+ });
46
+
47
+ it('should add multiple toasts', () => {
48
+ const { result } = renderHook(() => useToasts());
49
+
50
+ act(() => {
51
+ result.current.addToast('First message', 'info');
52
+ result.current.addToast('Second message', 'error');
53
+ result.current.addToast('Third message', 'success');
54
+ });
55
+
56
+ expect(result.current.toasts).toHaveLength(3);
57
+ });
58
+
59
+ it('should auto-remove toast after 4000ms', async () => {
60
+ vi.useRealTimers();
61
+ const { result } = renderHook(() => useToasts());
62
+
63
+ act(() => {
64
+ result.current.addToast('Auto-remove toast');
65
+ });
66
+
67
+ expect(result.current.toasts).toHaveLength(1);
68
+
69
+ await act(async () => {
70
+ await new Promise(resolve => setTimeout(resolve, 4001));
71
+ });
72
+
73
+ expect(result.current.toasts).toHaveLength(0);
74
+ });
75
+
76
+ it('should manually remove a toast', async () => {
77
+ vi.useRealTimers();
78
+ const { result } = renderHook(() => useToasts());
79
+
80
+ act(() => {
81
+ result.current.addToast('Test toast');
82
+ });
83
+
84
+ expect(result.current.toasts).toHaveLength(1);
85
+
86
+ const toastId = result.current.toasts[0].id;
87
+
88
+ act(() => {
89
+ result.current.removeToast(toastId);
90
+ });
91
+
92
+ expect(result.current.toasts).toHaveLength(0);
93
+ });
94
+
95
+ it('should handle all toast types', () => {
96
+ const types = ['info', 'success', 'warning', 'error'];
97
+ const { result } = renderHook(() => useToasts());
98
+
99
+ types.forEach((type, index) => {
100
+ act(() => {
101
+ result.current.addToast(`Message ${index}`, type);
102
+ });
103
+ });
104
+
105
+ expect(result.current.toasts).toHaveLength(4);
106
+ types.forEach((type, index) => {
107
+ expect(result.current.toasts[index].type).toBe(type);
108
+ });
109
+ });
110
+ });