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.
- package/AGENTS.md +218 -0
- package/README.md +64 -0
- package/backend/.env.example +1 -0
- package/backend/__tests__/getJiraClient.test.js +57 -0
- package/backend/__tests__/issues.test.js +565 -0
- package/backend/__tests__/jiraService.test.js +1127 -0
- package/backend/__tests__/projects.test.js +256 -0
- package/backend/coverage/clover.xml +426 -0
- package/backend/coverage/coverage-final.json +4 -0
- package/backend/coverage/lcov-report/base.css +224 -0
- package/backend/coverage/lcov-report/block-navigation.js +87 -0
- package/backend/coverage/lcov-report/favicon.png +0 -0
- package/backend/coverage/lcov-report/index.html +131 -0
- package/backend/coverage/lcov-report/prettify.css +1 -0
- package/backend/coverage/lcov-report/prettify.js +2 -0
- package/backend/coverage/lcov-report/routes/index.html +131 -0
- package/backend/coverage/lcov-report/routes/issues.js.html +823 -0
- package/backend/coverage/lcov-report/routes/projects.js.html +190 -0
- package/backend/coverage/lcov-report/service/index.html +116 -0
- package/backend/coverage/lcov-report/service/jiraService.js.html +1663 -0
- package/backend/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/backend/coverage/lcov-report/sorter.js +210 -0
- package/backend/coverage/lcov.info +707 -0
- package/backend/index.js +38 -0
- package/backend/jest.config.js +11 -0
- package/backend/package-lock.json +5636 -0
- package/backend/package.json +28 -0
- package/backend/routes/issues.js +246 -0
- package/backend/routes/projects.js +35 -0
- package/backend/service/jiraService.js +526 -0
- package/bin/jira.js +92 -0
- package/frontend/.env.example +1 -0
- package/frontend/coverage/base.css +224 -0
- package/frontend/coverage/block-navigation.js +87 -0
- package/frontend/coverage/clover.xml +559 -0
- package/frontend/coverage/components/CreateIssueModal.jsx.html +592 -0
- package/frontend/coverage/components/IssueDetailPanel.jsx.html +1633 -0
- package/frontend/coverage/components/IssueDrawer.jsx.html +550 -0
- package/frontend/coverage/components/IssueTable.jsx.html +571 -0
- package/frontend/coverage/components/SkeletonComponents.jsx.html +223 -0
- package/frontend/coverage/components/ToastContainer.jsx.html +142 -0
- package/frontend/coverage/components/index.html +191 -0
- package/frontend/coverage/coverage-final.json +14 -0
- package/frontend/coverage/favicon.png +0 -0
- package/frontend/coverage/hooks/index.html +161 -0
- package/frontend/coverage/hooks/useFocusTrap.js.html +262 -0
- package/frontend/coverage/hooks/useIssueDrawer.js.html +1000 -0
- package/frontend/coverage/hooks/useIssuesList.js.html +175 -0
- package/frontend/coverage/hooks/useToasts.js.html +142 -0
- package/frontend/coverage/index.html +161 -0
- package/frontend/coverage/prettify.css +1 -0
- package/frontend/coverage/prettify.js +2 -0
- package/frontend/coverage/services/api.js.html +547 -0
- package/frontend/coverage/services/index.html +116 -0
- package/frontend/coverage/sort-arrow-sprite.png +0 -0
- package/frontend/coverage/sorter.js +210 -0
- package/frontend/coverage/utils/index.html +131 -0
- package/frontend/coverage/utils/issueHelpers.jsx.html +334 -0
- package/frontend/coverage/utils/sanitize.js.html +166 -0
- package/frontend/index.html +13 -0
- package/frontend/package-lock.json +3436 -0
- package/frontend/package.json +30 -0
- package/frontend/src/App.jsx +447 -0
- package/frontend/src/__tests__/components/CreateIssueModal.test.jsx +375 -0
- package/frontend/src/__tests__/components/IssueDetailPanel.test.jsx +962 -0
- package/frontend/src/__tests__/components/IssueDrawer.test.jsx +240 -0
- package/frontend/src/__tests__/components/IssueTable.test.jsx +423 -0
- package/frontend/src/__tests__/components/ToastContainer.test.jsx +196 -0
- package/frontend/src/__tests__/hooks/useFocusTrap.test.js +197 -0
- package/frontend/src/__tests__/hooks/useIssueDrawer.test.js +1053 -0
- package/frontend/src/__tests__/hooks/useIssuesList.test.js +175 -0
- package/frontend/src/__tests__/hooks/useToasts.test.js +110 -0
- package/frontend/src/__tests__/services/api.test.js +568 -0
- package/frontend/src/__tests__/setup.js +54 -0
- package/frontend/src/__tests__/utils/issueHelpers.test.jsx +336 -0
- package/frontend/src/__tests__/utils/sanitize.test.js +238 -0
- package/frontend/src/components/CreateIssueModal.jsx +169 -0
- package/frontend/src/components/ErrorBoundary.jsx +52 -0
- package/frontend/src/components/IssueDetailPanel.jsx +517 -0
- package/frontend/src/components/IssueDrawer.jsx +155 -0
- package/frontend/src/components/IssueTable.jsx +162 -0
- package/frontend/src/components/SkeletonComponents.jsx +46 -0
- package/frontend/src/components/StandaloneIssuePage.jsx +176 -0
- package/frontend/src/components/ToastContainer.jsx +19 -0
- package/frontend/src/hooks/useFocusTrap.js +59 -0
- package/frontend/src/hooks/useIssueDrawer.js +305 -0
- package/frontend/src/hooks/useIssuesList.js +30 -0
- package/frontend/src/hooks/useToasts.js +19 -0
- package/frontend/src/index.css +2070 -0
- package/frontend/src/main.jsx +13 -0
- package/frontend/src/services/api.js +154 -0
- package/frontend/src/utils/issueHelpers.jsx +84 -0
- package/frontend/src/utils/sanitize.js +27 -0
- package/frontend/vite.config.js +15 -0
- 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
|
+
});
|