@conorheffron/ironoc-frontend 7.3.8 → 7.3.9
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/package.json +2 -1
- package/src/App.css +4 -0
- package/src/App.test.js +2 -1
- package/src/Footer.js +3 -1
- package/src/components/CoffeeCarousel.js +14 -1
- package/src/components/RepoIssues.js +152 -99
- package/src/components/__tests__/CoffeCarousel.test.js +2 -2
- package/src/components/__tests__/RepoIssues.test.js +94 -109
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conorheffron/ironoc-frontend",
|
|
3
|
-
"version": "7.3.
|
|
3
|
+
"version": "7.3.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@apollo/client": "^3.13.8",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"axios": "^1.8.2",
|
|
11
11
|
"bootstrap": "5.1",
|
|
12
12
|
"history": "^5.3.0",
|
|
13
|
+
"material-react-table": "^3.2.1",
|
|
13
14
|
"react": "^18.3.1",
|
|
14
15
|
"react-bootstrap": "^2.10.5",
|
|
15
16
|
"react-bootstrap-carousel": "^4.1.1",
|
package/src/App.css
CHANGED
package/src/App.test.js
CHANGED
|
@@ -96,7 +96,8 @@ describe('Footer Component', () => {
|
|
|
96
96
|
test('fetches and displays the version', async () => {
|
|
97
97
|
render(<Footer />);
|
|
98
98
|
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
|
|
99
|
-
await waitFor(() => expect(screen.getByText('© 2025 by Conor Heffron |
|
|
99
|
+
await waitFor(() => expect(screen.getByText('© 2025 by Conor Heffron |')).toBeInTheDocument());
|
|
100
|
+
await waitFor(() => expect(screen.getByText('2.2.0')).toBeInTheDocument());
|
|
100
101
|
});
|
|
101
102
|
|
|
102
103
|
test('handles fetch error', async () => {
|
package/src/Footer.js
CHANGED
|
@@ -29,7 +29,9 @@ class Footer extends Component {
|
|
|
29
29
|
return (
|
|
30
30
|
<div className="App">
|
|
31
31
|
<Container>
|
|
32
|
-
<footer><p className="ft"
|
|
32
|
+
<footer><p className="ft">
|
|
33
|
+
© 2025 by Conor Heffron | <a href="https://github.com/conorheffron/ironoc"
|
|
34
|
+
target="_blank" rel="noreferrer">{version}</a></p></footer>
|
|
33
35
|
</Container>
|
|
34
36
|
</div>
|
|
35
37
|
);
|
|
@@ -2,13 +2,26 @@ import React, { Component } from 'react';
|
|
|
2
2
|
import { Carousel } from 'react-bootstrap';
|
|
3
3
|
import '.././App.css';
|
|
4
4
|
|
|
5
|
+
// Helper function to validate URLs
|
|
6
|
+
function isValidUrl(url) {
|
|
7
|
+
try {
|
|
8
|
+
const parsed = new URL(url);
|
|
9
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
10
|
+
} catch (e) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
5
15
|
class CoffeeCarousel extends Component {
|
|
6
16
|
render() {
|
|
7
17
|
const { items } = this.props;
|
|
8
18
|
|
|
19
|
+
// Only include items with a valid http/https URL
|
|
20
|
+
const validItems = items.filter(item => isValidUrl(item.image));
|
|
21
|
+
|
|
9
22
|
return (
|
|
10
23
|
<Carousel className="App-header">
|
|
11
|
-
{
|
|
24
|
+
{validItems.map((item, index) => (
|
|
12
25
|
<Carousel.Item key={index}>
|
|
13
26
|
<img src={item.image} alt={item.title} />
|
|
14
27
|
<Carousel.Caption>
|
|
@@ -1,124 +1,177 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
3
|
-
import '
|
|
4
|
-
import
|
|
5
|
-
import AppNavbar from '.././AppNavbar';
|
|
6
|
-
import LoadingSpinner from '.././LoadingSpinner';
|
|
1
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import { Container, InputGroup, Form, Button } from 'react-bootstrap';
|
|
3
|
+
import AppNavbar from '../AppNavbar';
|
|
4
|
+
import LoadingSpinner from '../LoadingSpinner';
|
|
7
5
|
import { useParams, useNavigate } from 'react-router';
|
|
6
|
+
import {
|
|
7
|
+
MaterialReactTable,
|
|
8
|
+
useMaterialReactTable,
|
|
9
|
+
type MRT_ColumnDef,
|
|
10
|
+
} from 'material-react-table';
|
|
11
|
+
import { darken, lighten, useTheme } from '@mui/material';
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const navigate = useNavigate();
|
|
14
|
-
return <Component {...props} params={params} navigate={navigate} />;
|
|
15
|
-
};
|
|
16
|
-
}
|
|
13
|
+
const RepoIssues = () => {
|
|
14
|
+
const params = useParams();
|
|
15
|
+
const navigate = useNavigate();
|
|
16
|
+
const { id = '', repo = '' } = params;
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
this.state = {
|
|
22
|
-
repoIssueList: [],
|
|
23
|
-
isLoading: true,
|
|
24
|
-
value: ''
|
|
25
|
-
};
|
|
18
|
+
const [repoIssueList, setRepoIssueList] = useState([]);
|
|
19
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
20
|
+
const [value, setValue] = useState('');
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const fetchIssues = async () => {
|
|
24
|
+
if (id && repo) {
|
|
25
|
+
const response = await fetch(`/api/get-repo-issue/${id}/${repo}/`);
|
|
26
|
+
const body = await response.json();
|
|
27
|
+
setRepoIssueList(body);
|
|
28
|
+
}
|
|
29
|
+
setIsLoading(false);
|
|
30
|
+
};
|
|
31
|
+
fetchIssues();
|
|
32
|
+
}, [id, repo]);
|
|
30
33
|
|
|
31
|
-
handleChange(event)
|
|
32
|
-
this.setState({ value: event.target.value });
|
|
33
|
-
}
|
|
34
|
+
const handleChange = event => setValue(event.target.value);
|
|
34
35
|
|
|
35
|
-
onSubmit
|
|
36
|
+
const onSubmit = event => {
|
|
36
37
|
event.preventDefault();
|
|
37
|
-
|
|
38
|
-
const { id } = this.props.params;
|
|
39
|
-
this.props.navigate(`/issues/${id}/${value}`, {
|
|
38
|
+
navigate(`/issues/${id}/${value}`, {
|
|
40
39
|
replace: true,
|
|
41
40
|
state: {
|
|
42
41
|
id: id,
|
|
43
42
|
repo: value
|
|
44
43
|
}
|
|
45
44
|
});
|
|
46
|
-
|
|
47
|
-
}
|
|
45
|
+
navigate(0);
|
|
46
|
+
};
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
const { id, repo } = this.props.params;
|
|
51
|
-
if (id && repo) {
|
|
52
|
-
const response = await fetch(`/api/get-repo-issue/${id}/${repo}/`);
|
|
53
|
-
const body = await response.json();
|
|
54
|
-
this.setState({ repoIssueList: body, isLoading: false });
|
|
55
|
-
} else {
|
|
56
|
-
this.setState({ isLoading: false });
|
|
57
|
-
}
|
|
58
|
-
}
|
|
48
|
+
const projectLink = `/projects/${id}/`;
|
|
59
49
|
|
|
60
|
-
|
|
61
|
-
const { repoIssueList = [], isLoading = true, value = '' } = this.state;
|
|
62
|
-
const { id = '', repo = '' } = this.props.params;
|
|
50
|
+
const issueTags = ['enhancement', 'bug', 'java', 'javascript', 'release', 'ui', 'frontend', 'infra', 'dependencies', 'python-code']
|
|
63
51
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
52
|
+
// MaterialReactTable columns
|
|
53
|
+
const columns = useMemo(() => [
|
|
54
|
+
{
|
|
55
|
+
accessorKey: 'number',
|
|
56
|
+
header: 'Issue No.',
|
|
57
|
+
Cell: ({ cell }) => {
|
|
58
|
+
const issueLink = `https://github.com/${id}/${repo}/issues/${cell.getValue()}/`;
|
|
59
|
+
return (
|
|
60
|
+
<a href={issueLink} target="_blank" rel="noreferrer">
|
|
61
|
+
<b><i>{cell.getValue()}</i></b>
|
|
62
|
+
</a>
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
size: 40,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
accessorKey: 'state',
|
|
69
|
+
header: 'State',
|
|
70
|
+
filterVariant: 'multi-select',
|
|
71
|
+
size: 40,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
accessorKey: 'labels',
|
|
75
|
+
header: 'Labels',
|
|
76
|
+
filterVariant: 'multi-select',
|
|
77
|
+
filterSelectOptions: issueTags,
|
|
78
|
+
Cell: ({ cell }) => <b>{cell.getValue().join(', ')}</b>,
|
|
79
|
+
size: 100,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
accessorKey: 'title',
|
|
83
|
+
header: 'Title',
|
|
84
|
+
size: 150,
|
|
85
|
+
Cell: ({ cell }) => <b>{cell.getValue()}</b>,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
accessorKey: 'body',
|
|
89
|
+
header: 'Description',
|
|
90
|
+
size: 200,
|
|
91
|
+
},
|
|
92
|
+
], [id, repo]);
|
|
75
93
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
<td><b>{issue.title}</b></td>
|
|
84
|
-
<td>{issue.body}</td>
|
|
85
|
-
</tr>
|
|
86
|
-
);
|
|
87
|
-
});
|
|
94
|
+
const theme = useTheme();
|
|
95
|
+
|
|
96
|
+
//light or dark green
|
|
97
|
+
const baseBackgroundColor =
|
|
98
|
+
theme.palette.mode === 'dark'
|
|
99
|
+
? 'rgba(3, 44, 43, 1)'
|
|
100
|
+
: 'rgba(244, 255, 233, 1)';
|
|
88
101
|
|
|
89
|
-
|
|
102
|
+
const table = useMaterialReactTable({
|
|
103
|
+
columns,
|
|
104
|
+
data: repoIssueList,
|
|
105
|
+
enableFacetedValues: true,
|
|
106
|
+
enableStickyHeader: true,
|
|
107
|
+
initialState: { showColumnFilters: true },
|
|
108
|
+
muiTablePaperProps: {
|
|
109
|
+
elevation: 0,
|
|
110
|
+
sx: {
|
|
111
|
+
borderRadius: '0',
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
muiTableBodyProps: {
|
|
115
|
+
sx: (theme) => ({
|
|
116
|
+
'& tr:nth-of-type(odd):not([data-selected="true"]):not([data-pinned="true"]) > td':
|
|
117
|
+
{
|
|
118
|
+
backgroundColor: darken(baseBackgroundColor, 0.1),
|
|
119
|
+
},
|
|
120
|
+
'& tr:nth-of-type(odd):not([data-selected="true"]):not([data-pinned="true"]):hover > td':
|
|
121
|
+
{
|
|
122
|
+
backgroundColor: darken(baseBackgroundColor, 0.2),
|
|
123
|
+
},
|
|
124
|
+
'& tr:nth-of-type(even):not([data-selected="true"]):not([data-pinned="true"]) > td':
|
|
125
|
+
{
|
|
126
|
+
backgroundColor: lighten(baseBackgroundColor, 0.1),
|
|
127
|
+
},
|
|
128
|
+
'& tr:nth-of-type(even):not([data-selected="true"]):not([data-pinned="true"]):hover > td':
|
|
129
|
+
{
|
|
130
|
+
backgroundColor: darken(baseBackgroundColor, 0.2),
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
},
|
|
134
|
+
mrtTheme: (theme) => ({
|
|
135
|
+
baseBackgroundColor: baseBackgroundColor,
|
|
136
|
+
draggingBorderColor: theme.palette.secondary.main,
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
90
139
|
|
|
140
|
+
if (isLoading) {
|
|
91
141
|
return (
|
|
92
|
-
<div>
|
|
93
|
-
<AppNavbar
|
|
94
|
-
<Container
|
|
95
|
-
<br />
|
|
96
|
-
<
|
|
97
|
-
<Form.Control
|
|
98
|
-
placeholder="Enter Project Name... Example: ironoc-db"
|
|
99
|
-
aria-label="Enter Project Name..."
|
|
100
|
-
aria-describedby="basic-addon2"
|
|
101
|
-
type="text"
|
|
102
|
-
value={value}
|
|
103
|
-
onChange={this.handleChange}
|
|
104
|
-
/>
|
|
105
|
-
<Button color="primary" variant="outline-secondary" id="button-addon2" onClick={this.onSubmit}>Search Issues</Button>
|
|
106
|
-
</InputGroup>
|
|
107
|
-
<h3 className="table-headers">Issues for project <b>{repo}</b> and account <a href={projectLink}><b>{id}</b></a></h3>
|
|
108
|
-
<Table striped hover bordered>
|
|
109
|
-
<thead>
|
|
110
|
-
<tr className="table-secondary">
|
|
111
|
-
<th width="3%">Issue No.</th>
|
|
112
|
-
<th width="27%">Title</th>
|
|
113
|
-
<th width="70%">Description</th>
|
|
114
|
-
</tr>
|
|
115
|
-
</thead>
|
|
116
|
-
<tbody>{repoList}</tbody>
|
|
117
|
-
</Table>
|
|
142
|
+
<div className="App">
|
|
143
|
+
<AppNavbar />
|
|
144
|
+
<Container>
|
|
145
|
+
<br /><br /><br />
|
|
146
|
+
<LoadingSpinner />
|
|
118
147
|
</Container>
|
|
119
148
|
</div>
|
|
120
149
|
);
|
|
121
150
|
}
|
|
122
|
-
}
|
|
123
151
|
|
|
124
|
-
|
|
152
|
+
return (
|
|
153
|
+
<div>
|
|
154
|
+
<AppNavbar /><br /><br />
|
|
155
|
+
<Container fluid>
|
|
156
|
+
<br />
|
|
157
|
+
<InputGroup className="mb-3">
|
|
158
|
+
<Form.Control
|
|
159
|
+
placeholder="Enter Project Name... Example: ironoc-db"
|
|
160
|
+
aria-label="Enter Project Name..."
|
|
161
|
+
aria-describedby="basic-addon2"
|
|
162
|
+
type="text"
|
|
163
|
+
value={value}
|
|
164
|
+
onChange={handleChange}
|
|
165
|
+
/>
|
|
166
|
+
<Button color="primary" variant="outline-secondary" id="button-addon2" onClick={onSubmit}>Search Issues</Button>
|
|
167
|
+
</InputGroup>
|
|
168
|
+
<h3 className="table-headers">
|
|
169
|
+
Issues for project <b>{repo}</b> and account <a href={projectLink}><b>{id}</b></a>
|
|
170
|
+
</h3>
|
|
171
|
+
<MaterialReactTable table={table} />
|
|
172
|
+
</Container>
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export default RepoIssues;
|
|
@@ -40,12 +40,12 @@ describe('CoffeeCarousel', () => {
|
|
|
40
40
|
test('does not render ingredients if item.ingredients is null or an empty array', () => {
|
|
41
41
|
const mockItems = [
|
|
42
42
|
{
|
|
43
|
-
image: 'image1.jpg',
|
|
43
|
+
image: 'http://image1.jpg',
|
|
44
44
|
title: 'Coffee with no ingredients',
|
|
45
45
|
ingredients: [],
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
|
-
image: 'image2.jpg',
|
|
48
|
+
image: 'https://image2.jpg',
|
|
49
49
|
title: 'Coffee with null ingredients',
|
|
50
50
|
ingredients: null,
|
|
51
51
|
},
|
|
@@ -1,121 +1,106 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
|
-
import { MemoryRouter } from 'react-router';
|
|
4
3
|
import RepoIssues from '../RepoIssues';
|
|
5
4
|
|
|
5
|
+
// Mock react-router
|
|
6
6
|
jest.mock('react-router', () => ({
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
useNavigate: jest.fn(),
|
|
7
|
+
useParams: jest.fn(),
|
|
8
|
+
useNavigate: jest.fn(),
|
|
10
9
|
}));
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
Promise.resolve({
|
|
22
|
-
json: () =>
|
|
23
|
-
Promise.resolve([
|
|
24
|
-
{
|
|
25
|
-
number: 1,
|
|
26
|
-
title: 'Test Issue Title',
|
|
27
|
-
body: 'Test issue body description',
|
|
28
|
-
},
|
|
29
|
-
]),
|
|
30
|
-
})
|
|
31
|
-
);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
afterEach(() => {
|
|
35
|
-
jest.clearAllMocks();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test('renders loading spinner initially', () => {
|
|
39
|
-
render(
|
|
40
|
-
<MemoryRouter>
|
|
41
|
-
<RepoIssues />
|
|
42
|
-
</MemoryRouter>
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
expect(screen.getByText(/loading/i)).toBeInTheDocument();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test('fetches and displays repo issues', async () => {
|
|
49
|
-
render(
|
|
50
|
-
<MemoryRouter>
|
|
51
|
-
<RepoIssues />
|
|
52
|
-
</MemoryRouter>
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
|
|
56
|
-
|
|
57
|
-
expect(screen.getByText(/Test Issue Title/i)).toBeInTheDocument();
|
|
58
|
-
expect(screen.getByText(/Test issue body description/i)).toBeInTheDocument();
|
|
59
|
-
expect(screen.getByText(/1/i)).toBeInTheDocument();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('handles input change', async () => {
|
|
63
|
-
render(
|
|
64
|
-
<MemoryRouter>
|
|
65
|
-
<RepoIssues />
|
|
66
|
-
</MemoryRouter>
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
|
|
70
|
-
|
|
71
|
-
const input = screen.getByPlaceholderText(/Enter Project Name/i);
|
|
72
|
-
fireEvent.change(input, { target: { value: 'newRepo' } });
|
|
73
|
-
|
|
74
|
-
expect(input.value).toBe('newRepo');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('navigates on form submission', async () => {
|
|
78
|
-
render(
|
|
79
|
-
<MemoryRouter>
|
|
80
|
-
<RepoIssues />
|
|
81
|
-
</MemoryRouter>
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
|
|
85
|
-
|
|
86
|
-
const input = screen.getByPlaceholderText(/Enter Project Name/i);
|
|
87
|
-
const button = screen.getByText(/Search Issues/i);
|
|
88
|
-
|
|
89
|
-
fireEvent.change(input, { target: { value: 'newRepo' } });
|
|
90
|
-
fireEvent.click(button);
|
|
91
|
-
|
|
92
|
-
expect(mockNavigate).toHaveBeenCalledWith('/issues/testUser/newRepo', {
|
|
93
|
-
replace: true,
|
|
94
|
-
state: {
|
|
95
|
-
id: 'testUser',
|
|
96
|
-
repo: 'newRepo',
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test('displays table headers correctly', async () => {
|
|
102
|
-
render(
|
|
103
|
-
<MemoryRouter>
|
|
104
|
-
<RepoIssues />
|
|
105
|
-
</MemoryRouter>
|
|
106
|
-
);
|
|
11
|
+
// Mock react-bootstrap
|
|
12
|
+
jest.mock('react-bootstrap', () => ({
|
|
13
|
+
Container: ({ children, ...props }) => <div data-testid="container" {...props}>{children}</div>,
|
|
14
|
+
InputGroup: ({ children, ...props }) => <div data-testid="input-group" {...props}>{children}</div>,
|
|
15
|
+
Form: {
|
|
16
|
+
Control: ({ ...props }) => <input data-testid="form-control" {...props} />,
|
|
17
|
+
},
|
|
18
|
+
Button: ({ children, ...props }) => <button {...props}>{children}</button>,
|
|
19
|
+
}));
|
|
107
20
|
|
|
108
|
-
|
|
109
|
-
|
|
21
|
+
// Mock AppNavbar and LoadingSpinner
|
|
22
|
+
jest.mock('../../AppNavbar', () => () => <div data-testid="navbar">Navbar</div>);
|
|
23
|
+
jest.mock('../../LoadingSpinner', () => () => <div data-testid="spinner">Loading...</div>);
|
|
24
|
+
|
|
25
|
+
// Mock MaterialReactTable and useMaterialReactTable
|
|
26
|
+
jest.mock('material-react-table', () => ({
|
|
27
|
+
MaterialReactTable: ({ table }) => (
|
|
28
|
+
<div data-testid="mrt-table">
|
|
29
|
+
{table && table.data && table.data.map((issue, idx) => (
|
|
30
|
+
<div key={idx} data-testid="mrt-row">{issue.title}</div>
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
),
|
|
34
|
+
useMaterialReactTable: jest.fn((opts) => opts),
|
|
35
|
+
}));
|
|
110
36
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
37
|
+
// Mock @mui/material theme functions
|
|
38
|
+
jest.mock('@mui/material', () => ({
|
|
39
|
+
useTheme: () => ({
|
|
40
|
+
palette: {
|
|
41
|
+
mode: 'light',
|
|
42
|
+
secondary: { main: '#00ff00' }
|
|
43
|
+
}
|
|
44
|
+
}),
|
|
45
|
+
darken: (color, amount) => color + '-darken' + amount,
|
|
46
|
+
lighten: (color, amount) => color + '-lighten' + amount,
|
|
47
|
+
}));
|
|
115
48
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
49
|
+
import { useParams, useNavigate } from 'react-router';
|
|
50
|
+
|
|
51
|
+
describe('RepoIssues', () => {
|
|
52
|
+
const mockNavigate = jest.fn();
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
jest.clearAllMocks();
|
|
56
|
+
useNavigate.mockReturnValue(mockNavigate);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('shows loading spinner and navbar initially', () => {
|
|
60
|
+
useParams.mockReturnValue({ id: 'user', repo: 'repo' });
|
|
61
|
+
render(<RepoIssues />);
|
|
62
|
+
expect(screen.getByTestId('navbar')).toBeInTheDocument();
|
|
63
|
+
expect(screen.getByTestId('spinner')).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('fetches and displays issues in the table after loading', async () => {
|
|
67
|
+
useParams.mockReturnValue({ id: 'user', repo: 'repo' });
|
|
68
|
+
global.fetch = jest.fn(() =>
|
|
69
|
+
Promise.resolve({
|
|
70
|
+
json: () => Promise.resolve([
|
|
71
|
+
{
|
|
72
|
+
number: 1,
|
|
73
|
+
state: 'open',
|
|
74
|
+
labels: ['bug'],
|
|
75
|
+
title: 'Test Issue',
|
|
76
|
+
body: 'Body here',
|
|
77
|
+
},
|
|
78
|
+
]),
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
render(<RepoIssues />);
|
|
82
|
+
await waitFor(() =>
|
|
83
|
+
expect(screen.queryByTestId('spinner')).not.toBeInTheDocument()
|
|
84
|
+
);
|
|
85
|
+
expect(screen.getByTestId('mrt-table')).toBeInTheDocument();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('navigates on form submit', async () => {
|
|
89
|
+
useParams.mockReturnValue({ id: 'user', repo: 'repo' });
|
|
90
|
+
global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve([]) }));
|
|
91
|
+
render(<RepoIssues />);
|
|
92
|
+
await waitFor(() => expect(screen.queryByTestId('spinner')).not.toBeInTheDocument());
|
|
93
|
+
const input = screen.getByTestId('form-control');
|
|
94
|
+
const button = screen.getByText(/Search Issues/i);
|
|
95
|
+
fireEvent.change(input, { target: { value: 'newrepo' } });
|
|
96
|
+
fireEvent.click(button);
|
|
97
|
+
expect(mockNavigate).toHaveBeenCalledWith('/issues/user/newrepo', {
|
|
98
|
+
replace: true,
|
|
99
|
+
state: {
|
|
100
|
+
id: 'user',
|
|
101
|
+
repo: 'newrepo',
|
|
102
|
+
},
|
|
120
103
|
});
|
|
104
|
+
expect(mockNavigate).toHaveBeenCalledWith(0);
|
|
105
|
+
});
|
|
121
106
|
});
|