-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathGitHubIssueImportModal.test.tsx
More file actions
201 lines (173 loc) · 6.71 KB
/
Copy pathGitHubIssueImportModal.test.tsx
File metadata and controls
201 lines (173 loc) · 6.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import useSWR from 'swr';
import { GitHubIssueImportModal } from '@/components/tasks/GitHubIssueImportModal';
import type { GitHubIssue, GitHubIssuesResponse } from '@/types';
jest.mock('swr');
jest.mock('@/lib/api', () => ({
integrationsApi: {
getIssues: jest.fn(),
},
}));
const mockUseSWR = useSWR as jest.MockedFunction<typeof useSWR>;
function issue(number: number, title: string, extra: Partial<GitHubIssue> = {}): GitHubIssue {
return {
number,
title,
labels: extra.labels ?? [],
assignee: extra.assignee ?? null,
created_at: extra.created_at ?? '2026-05-01T12:00:00Z',
html_url: `https://github.com/acme/app/issues/${number}`,
};
}
/**
* Drive useSWR's return value off the key the component passes. The key is
* `['github-issues', workspacePath, page, search, label]`, so this lets tests
* return different pages for different `page` values — needed to verify that
* selection persists across page changes.
*/
function mockSWRByPage(pages: Record<number, GitHubIssue[]>, total: number) {
mockUseSWR.mockImplementation((key) => {
if (!key) {
return { data: undefined, error: undefined, isLoading: false } as never;
}
const page = (key as unknown[])[2] as number;
const resp: GitHubIssuesResponse = {
issues: pages[page] ?? [],
total,
page,
per_page: 25,
};
return { data: resp, error: undefined, isLoading: false } as never;
});
}
const baseProps = {
open: true,
workspacePath: '/ws',
repo: 'acme/app',
onClose: jest.fn(),
onImport: jest.fn(),
};
describe('GitHubIssueImportModal', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('renders issue rows with title, labels, assignee', () => {
mockSWRByPage(
{ 1: [issue(42, 'Fix login bug', { labels: ['bug'], assignee: 'alice' })] },
1
);
render(<GitHubIssueImportModal {...baseProps} />);
expect(screen.getByText('Fix login bug')).toBeInTheDocument();
expect(screen.getByText('#42')).toBeInTheDocument();
expect(screen.getByText('bug')).toBeInTheDocument();
expect(screen.getByText('@alice')).toBeInTheDocument();
});
it('disables Import Selected until at least one issue is selected', () => {
mockSWRByPage({ 1: [issue(42, 'Fix login bug')] }, 1);
render(<GitHubIssueImportModal {...baseProps} />);
const importBtn = screen.getByRole('button', { name: /import selected/i });
expect(importBtn).toBeDisabled();
fireEvent.click(screen.getByLabelText('Select issue #42'));
expect(importBtn).toBeEnabled();
});
it('calls onImport with the selected issues', () => {
const onImport = jest.fn();
mockSWRByPage(
{ 1: [issue(42, 'Fix login bug'), issue(41, 'Add dark mode')] },
2
);
render(<GitHubIssueImportModal {...baseProps} onImport={onImport} />);
fireEvent.click(screen.getByLabelText('Select issue #41'));
fireEvent.click(screen.getByRole('button', { name: /import selected/i }));
expect(onImport).toHaveBeenCalledTimes(1);
const passed = onImport.mock.calls[0][0] as GitHubIssue[];
expect(passed.map((i) => i.number)).toEqual([41]);
});
it('shows a selected-count badge that reflects selections', () => {
mockSWRByPage(
{ 1: [issue(42, 'A'), issue(41, 'B')] },
2
);
render(<GitHubIssueImportModal {...baseProps} />);
expect(screen.queryByText(/selected/)).not.toBeInTheDocument();
fireEvent.click(screen.getByLabelText('Select issue #42'));
fireEvent.click(screen.getByLabelText('Select issue #41'));
expect(screen.getByText('2 selected')).toBeInTheDocument();
});
it('select-all-on-page selects every visible issue', () => {
mockSWRByPage(
{ 1: [issue(42, 'A'), issue(41, 'B'), issue(40, 'C')] },
3
);
render(<GitHubIssueImportModal {...baseProps} />);
fireEvent.click(screen.getByLabelText('Select all on page'));
expect(screen.getByText('3 selected')).toBeInTheDocument();
});
it('persists selection across page changes', async () => {
mockSWRByPage(
{
1: [issue(42, 'Page one issue')],
2: [issue(10, 'Page two issue')],
},
30 // > 25 so a second page exists
);
render(<GitHubIssueImportModal {...baseProps} />);
// Select an issue on page 1.
fireEvent.click(screen.getByLabelText('Select issue #42'));
expect(screen.getByText('1 selected')).toBeInTheDocument();
// Go to page 2 — different rows, but the count must persist.
fireEvent.click(screen.getByLabelText('Next page'));
await waitFor(() =>
expect(screen.getByText('Page 2 of 2')).toBeInTheDocument()
);
expect(screen.getByText('Page two issue')).toBeInTheDocument();
expect(screen.getByText('1 selected')).toBeInTheDocument();
// Selecting a page-2 issue accumulates rather than replaces.
fireEvent.click(screen.getByLabelText('Select issue #10'));
expect(screen.getByText('2 selected')).toBeInTheDocument();
});
it('renders pagination with the right total page count', () => {
mockSWRByPage({ 1: [issue(1, 'x')] }, 60); // 60 / 25 = 3 pages
render(<GitHubIssueImportModal {...baseProps} />);
expect(screen.getByText('Page 1 of 3')).toBeInTheDocument();
});
it('shows an error banner when the fetch fails', () => {
mockUseSWR.mockReturnValue({
data: undefined,
error: { detail: 'GitHub unreachable' },
isLoading: false,
} as never);
render(<GitHubIssueImportModal {...baseProps} />);
expect(screen.getByRole('alert')).toHaveTextContent('GitHub unreachable');
});
it('does not fetch when closed (SWR key is null)', () => {
mockUseSWR.mockReturnValue({
data: undefined,
error: undefined,
isLoading: false,
} as never);
render(<GitHubIssueImportModal {...baseProps} open={false} />);
// SWR is called with a null key when closed.
const lastCallKey = mockUseSWR.mock.calls.at(-1)?.[0];
expect(lastCallKey).toBeNull();
});
it('renders an inline import error and keeps the modal usable', () => {
mockSWRByPage({ 1: [issue(1, 'x')] }, 1);
render(
<GitHubIssueImportModal {...baseProps} importError="No GitHub repository is connected." />
);
expect(screen.getByRole('alert')).toHaveTextContent(
'No GitHub repository is connected.'
);
// The issue row is still rendered (selection preserved for a retry).
expect(screen.getByText('x')).toBeInTheDocument();
});
it('shows an importing progress label on the action button', () => {
mockSWRByPage({ 1: [issue(1, 'x')] }, 1);
render(<GitHubIssueImportModal {...baseProps} importing />);
expect(
screen.getByRole('button', { name: /importing/i })
).toBeInTheDocument();
});
});