@conorheffron/ironoc-frontend 9.1.6 → 9.1.8
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/README.md +2 -0
- package/package.json +8 -3
- package/src/App.css +104 -17
- package/src/App.js +2 -2
- package/src/App.test.js +6 -3
- package/src/components/CoffeeCarousel.js +60 -7
- package/src/components/Donate.js +40 -70
- package/src/components/RepoIssues.js +1 -0
- package/src/components/__tests__/CoffeCarousel.test.js +18 -0
- package/src/components/__tests__/CoffeeHome.test.js +7 -3
- package/src/components/__tests__/ControlledCarousel.test.js +7 -3
- package/src/components/__tests__/RepoDetails.test.js +21 -5
- package/src/components/__tests__/RepoIssues.test.js +44 -6
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
The ironoc UI built with react for personal portfolio website.
|
|
4
4
|
|
|
5
|
+
[](https://github.com/conorheffron/ironoc/actions/workflows/aws.yml)
|
|
6
|
+
|
|
5
7
|
[](https://github.com/conorheffron/ironoc/actions/workflows/npm-publish-packages.yml)
|
|
6
8
|
|
|
7
9
|
[](https://github.com/conorheffron/ironoc/actions/workflows/node.js.yml)
|
package/package.json
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conorheffron/ironoc-frontend",
|
|
3
|
-
"version": "9.1.
|
|
3
|
+
"version": "9.1.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@apollo/client": "^3.13.8",
|
|
8
8
|
"@emotion/is-prop-valid": "^1.4.0",
|
|
9
|
-
"@emotion/react": "
|
|
10
|
-
"@emotion/styled": "
|
|
9
|
+
"@emotion/react": "11.14.0",
|
|
10
|
+
"@emotion/styled": "11.14.1",
|
|
11
11
|
"@fontsource/montserrat": "^5.2.8",
|
|
12
12
|
"@fontsource/open-sans": "^5.2.7",
|
|
13
13
|
"@graphiql/plugin-explorer": "^5.1.1",
|
|
14
14
|
"@graphiql/react": "^0.37.1",
|
|
15
|
+
"@mui/icons-material": "7.3.0",
|
|
16
|
+
"@mui/material": "7.3.0",
|
|
17
|
+
"@react-aria/ssr": "3.9.10",
|
|
15
18
|
"@testing-library/user-event": "^14.6.1",
|
|
16
19
|
"axios": "^1.15.2",
|
|
17
20
|
"bootstrap": "5.3",
|
|
@@ -27,6 +30,7 @@
|
|
|
27
30
|
"react-dom": "^19.2.0",
|
|
28
31
|
"react-router": "^7.12.0",
|
|
29
32
|
"react-router-dom": "^7.12.0",
|
|
33
|
+
"react-transition-group": "4.4.5",
|
|
30
34
|
"reactstrap": "^9.2.3",
|
|
31
35
|
"recharts": "^3.3.0",
|
|
32
36
|
"web-vitals": "^5.1.0"
|
|
@@ -58,6 +62,7 @@
|
|
|
58
62
|
},
|
|
59
63
|
"devDependencies": {
|
|
60
64
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
|
65
|
+
"@testing-library/dom": "^10.4.1",
|
|
61
66
|
"@testing-library/jest-dom": "^6.9.1",
|
|
62
67
|
"@testing-library/react": "^16.3.0",
|
|
63
68
|
"jest": "^30.2.0",
|
package/src/App.css
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
background-color: #1D428A;
|
|
25
25
|
border-bottom-right-radius: 18%;
|
|
26
26
|
border-bottom-left-radius: 18%;
|
|
27
|
+
overflow: hidden;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
.App-link {
|
|
@@ -50,8 +51,8 @@
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
.carousel-caption {
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
top: 0;
|
|
55
|
+
bottom: auto;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
.App-intro {
|
|
@@ -60,23 +61,23 @@
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
#my-intro {
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
padding-left: 15%;
|
|
65
|
+
padding-right: 15%;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
.strava-badge {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
69
|
+
display: inline-block;
|
|
70
|
+
background-color: #FC5200;
|
|
71
|
+
color: #fff;
|
|
72
|
+
padding: 5px 10px 5px 30px;
|
|
73
|
+
font-size: 11px;
|
|
74
|
+
font-family: Helvetica, Arial, sans-serif;
|
|
75
|
+
white-space: nowrap;
|
|
76
|
+
text-decoration: none;
|
|
77
|
+
background-repeat: no-repeat;
|
|
78
|
+
background-position: 10px center;
|
|
79
|
+
border-radius: 3px;
|
|
80
|
+
background-image: url('https://badges.strava.com/logo-strava-echelon.png')
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
.strava-badge-img {
|
|
@@ -107,7 +108,7 @@ footer p a {
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
.carousel-caption h3, h5 {
|
|
110
|
-
background-color:yellow;
|
|
111
|
+
background-color: yellow;
|
|
111
112
|
width: 50%;
|
|
112
113
|
height: auto;
|
|
113
114
|
margin: 0 auto;
|
|
@@ -130,3 +131,89 @@ footer p a {
|
|
|
130
131
|
overflow: hidden;
|
|
131
132
|
text-overflow: ellipsis; /* Add ellipsis for text overflow */
|
|
132
133
|
}
|
|
134
|
+
|
|
135
|
+
.donate-carousel-item {
|
|
136
|
+
min-height: 90vh;
|
|
137
|
+
object-fit: cover;
|
|
138
|
+
border-bottom-right-radius: 18%;
|
|
139
|
+
border-bottom-left-radius: 18%;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.donate-carousel-bg {
|
|
143
|
+
min-height: 90vh;
|
|
144
|
+
object-fit: cover;
|
|
145
|
+
border-bottom-right-radius: 18%;
|
|
146
|
+
border-bottom-left-radius: 18%;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.donate-carousel-caption {
|
|
150
|
+
top: 50%;
|
|
151
|
+
bottom: auto;
|
|
152
|
+
left: 8%;
|
|
153
|
+
right: 8%;
|
|
154
|
+
transform: translateY(-50%);
|
|
155
|
+
padding: 1.25rem;
|
|
156
|
+
color: #ffffff;
|
|
157
|
+
background: rgba(0, 0, 0, 0.45);
|
|
158
|
+
border-radius: 1rem;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.donate-title {
|
|
162
|
+
font-size: clamp(1.5rem, 5vw, 3rem);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.donate-carousel-caption p {
|
|
166
|
+
font-size: clamp(0.9rem, 2.8vw, 1.2rem);
|
|
167
|
+
line-height: 1.45;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.donate-carousel-caption a {
|
|
171
|
+
color: yellow;
|
|
172
|
+
overflow-wrap: anywhere;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.donate-overview {
|
|
176
|
+
max-height: none;
|
|
177
|
+
overflow: visible;
|
|
178
|
+
text-overflow: initial;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@media (max-width: 576px) {
|
|
182
|
+
.donate-carousel-item {
|
|
183
|
+
min-height: auto;
|
|
184
|
+
border-bottom-right-radius: 18%;
|
|
185
|
+
border-bottom-left-radius: 18%;
|
|
186
|
+
overflow: hidden;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.donate-carousel-bg {
|
|
190
|
+
min-height: 720px;
|
|
191
|
+
border-bottom-right-radius: 18%;
|
|
192
|
+
border-bottom-left-radius: 18%;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.donate-carousel-caption {
|
|
196
|
+
top: 1rem;
|
|
197
|
+
left: 4%;
|
|
198
|
+
right: 4%;
|
|
199
|
+
transform: none;
|
|
200
|
+
padding: 0.85rem;
|
|
201
|
+
max-height: calc(100% - 2rem);
|
|
202
|
+
overflow-y: auto;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.donate-title {
|
|
206
|
+
font-size: 1.35rem;
|
|
207
|
+
margin-bottom: 0.75rem !important;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.donate-carousel-caption p {
|
|
211
|
+
font-size: 0.9rem;
|
|
212
|
+
line-height: 1.35;
|
|
213
|
+
margin-bottom: 0.65rem;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.donate-overview {
|
|
217
|
+
font-size: 0.85rem;
|
|
218
|
+
}
|
|
219
|
+
}
|
package/src/App.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { Component } from 'react';
|
|
2
2
|
import { BrowserRouter as Router, Routes, Route } from 'react-router';
|
|
3
|
-
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
|
|
3
|
+
import { ApolloClient, InMemoryCache, ApolloProvider, HttpLink } from '@apollo/client';
|
|
4
4
|
import './App.css';
|
|
5
5
|
import Home from './components/Home';
|
|
6
6
|
import CoffeeHome from './components/CoffeeHome';
|
|
@@ -31,7 +31,7 @@ class App extends Component {
|
|
|
31
31
|
|
|
32
32
|
// Create Apollo Client for '/donate' route
|
|
33
33
|
const donateClient = new ApolloClient({
|
|
34
|
-
uri: '/graphql',
|
|
34
|
+
link: new HttpLink({ uri: '/graphql' }),
|
|
35
35
|
cache: new InMemoryCache(),
|
|
36
36
|
});
|
|
37
37
|
|
package/src/App.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { render, screen, waitFor, fireEvent, act } from '@testing-library/react';
|
|
3
3
|
import '@testing-library/jest-dom';
|
|
4
4
|
import Home from './components/Home';
|
|
5
5
|
import App from './App';
|
|
@@ -108,12 +108,15 @@ describe('Footer Component', () => {
|
|
|
108
108
|
fetch.mockClear();
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
-
test('renders without crashing', () => {
|
|
112
|
-
|
|
111
|
+
test('renders without crashing', async () => {
|
|
112
|
+
await act(async () => {
|
|
113
|
+
render(<Footer />);
|
|
114
|
+
});
|
|
113
115
|
expect(screen.getByText(/© 2025 by Conor Heffron/)).toBeInTheDocument();
|
|
114
116
|
});
|
|
115
117
|
|
|
116
118
|
test('initial state is set correctly', () => {
|
|
119
|
+
global.fetch = jest.fn(() => new Promise(() => {}));
|
|
117
120
|
const { container } = render(<Footer />);
|
|
118
121
|
const footerText = container.querySelector('.ft');
|
|
119
122
|
expect(footerText).toHaveTextContent('© 2025 by Conor Heffron |');
|
|
@@ -12,6 +12,58 @@ function isValidUrl(url) {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
const brewTitleTranslations = {
|
|
16
|
+
'Svart Te': 'Black Tea',
|
|
17
|
+
'Islatte': 'Iced Latte',
|
|
18
|
+
'Islatte Mocha': 'Iced Mocha',
|
|
19
|
+
'Frapino Mocka': 'Frapino Mocha',
|
|
20
|
+
'Apelsinjuice': 'Orange Juice',
|
|
21
|
+
'Lemonad': 'Lemonade',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const ingredientTranslations = {
|
|
25
|
+
'Ångad mjölk': 'Steamed milk',
|
|
26
|
+
'Karamellsirap': 'Caramel syrup',
|
|
27
|
+
'Hett vatten': 'Hot water',
|
|
28
|
+
'Choklad': 'Chocolate',
|
|
29
|
+
'Mjölk': 'Milk',
|
|
30
|
+
'Te': 'Tea',
|
|
31
|
+
'Ingefära': 'Ginger',
|
|
32
|
+
'Kardemumma': 'Cardamom',
|
|
33
|
+
'Kanel': 'Cinnamon',
|
|
34
|
+
'Matcha-pulver': 'Matcha powder',
|
|
35
|
+
'Socker*': 'Sugar*',
|
|
36
|
+
'Kaffe': 'Coffee',
|
|
37
|
+
'Is': 'Ice',
|
|
38
|
+
'Sirap': 'Syrup',
|
|
39
|
+
'Vispgrädde*': 'Whipped cream*',
|
|
40
|
+
'Karamellsås': 'Caramel sauce',
|
|
41
|
+
'Färska Apelsiner': 'Fresh oranges',
|
|
42
|
+
'Citronsaft': 'Lemon juice',
|
|
43
|
+
'Kolsyrat vatten': 'Sparkling water',
|
|
44
|
+
'Honung': 'Honey',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function translateValue(value, translations) {
|
|
48
|
+
const trimmedValue = typeof value === 'string' ? value.trim() : value;
|
|
49
|
+
return translations[trimmedValue] || trimmedValue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function translateIngredients(ingredients) {
|
|
53
|
+
if (Array.isArray(ingredients)) {
|
|
54
|
+
return ingredients
|
|
55
|
+
.map((ingredient) => translateValue(ingredient, ingredientTranslations))
|
|
56
|
+
.join(', ');
|
|
57
|
+
}
|
|
58
|
+
if (typeof ingredients === 'string') {
|
|
59
|
+
return ingredients
|
|
60
|
+
.split(',')
|
|
61
|
+
.map((ingredient) => translateValue(ingredient, ingredientTranslations))
|
|
62
|
+
.join(', ');
|
|
63
|
+
}
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
|
|
15
67
|
class CoffeeCarousel extends Component {
|
|
16
68
|
render() {
|
|
17
69
|
const { items } = this.props;
|
|
@@ -21,22 +73,23 @@ class CoffeeCarousel extends Component {
|
|
|
21
73
|
);
|
|
22
74
|
return (
|
|
23
75
|
<Carousel className="App-header">
|
|
24
|
-
{validItems.map((item, index) =>
|
|
76
|
+
{validItems.map((item, index) => {
|
|
77
|
+
const translatedTitle = translateValue(item.title, brewTitleTranslations);
|
|
78
|
+
return (
|
|
25
79
|
<Carousel.Item key={index}>
|
|
26
|
-
<img src={item.image} alt={
|
|
80
|
+
<img src={item.image} alt={translatedTitle} />
|
|
27
81
|
<Carousel.Caption>
|
|
28
|
-
<h3>{
|
|
82
|
+
<h3>{translatedTitle}</h3>
|
|
29
83
|
{item.ingredients && item.ingredients.length > 0 && (
|
|
30
84
|
<h5>
|
|
31
85
|
<b>Ingredients:</b>{' '}
|
|
32
|
-
{
|
|
33
|
-
? item.ingredients.join(', ')
|
|
34
|
-
: item.ingredients}
|
|
86
|
+
{translateIngredients(item.ingredients)}
|
|
35
87
|
</h5>
|
|
36
88
|
)}
|
|
37
89
|
</Carousel.Caption>
|
|
38
90
|
</Carousel.Item>
|
|
39
|
-
|
|
91
|
+
);
|
|
92
|
+
})}
|
|
40
93
|
</Carousel>
|
|
41
94
|
);
|
|
42
95
|
}
|
package/src/components/Donate.js
CHANGED
|
@@ -79,82 +79,52 @@ class Donate extends Component {
|
|
|
79
79
|
);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
return (
|
|
83
|
-
<div className="App">
|
|
84
|
-
<AppNavbar />
|
|
85
|
-
<Container>
|
|
86
|
-
<Carousel className="App-header">
|
|
87
|
-
{donateItems.map((item, index) => (
|
|
88
|
-
<Carousel.Item key={index} interval={500}>
|
|
89
|
-
<img className="d-block w-100" src={red} alt={item.alt} />
|
|
90
|
-
|
|
91
|
-
<Carousel.Caption>
|
|
92
|
-
<h1 className="mb-3">
|
|
93
|
-
<span style={{ textDecoration: 'underline' }}>{item.name}</span>
|
|
94
|
-
</h1>
|
|
95
|
-
|
|
96
|
-
<p>
|
|
97
|
-
<b>Contact & Help by Phone: </b>
|
|
98
|
-
<span dangerouslySetInnerHTML={{ __html: item.phone }} />
|
|
99
|
-
</p>
|
|
100
|
-
|
|
101
|
-
<p>
|
|
102
|
-
<b>Home page: </b>
|
|
103
|
-
<a
|
|
104
|
-
href={item.link}
|
|
105
|
-
target="_blank"
|
|
106
|
-
rel="noreferrer"
|
|
107
|
-
onClick={() => trackClickOut('charity', item.link)}
|
|
108
|
-
>
|
|
109
|
-
{item.link}
|
|
110
|
-
</a>
|
|
111
|
-
</p>
|
|
112
|
-
|
|
113
|
-
<p className="overview-text">
|
|
114
|
-
<b>Overview:</b> Founded in {item.founded}, {item.overview}
|
|
115
|
-
</p>
|
|
116
|
-
|
|
117
|
-
<p>
|
|
118
|
-
<a
|
|
119
|
-
href={item.donate}
|
|
120
|
-
target="_blank"
|
|
121
|
-
rel="noreferrer"
|
|
122
|
-
onClick={() => trackClickOut('charity', item.donate)}
|
|
123
|
-
>
|
|
124
|
-
Donate here
|
|
125
|
-
</a>
|
|
126
|
-
</p>
|
|
127
|
-
</Carousel.Caption>
|
|
128
|
-
</Carousel.Item>
|
|
129
|
-
))}
|
|
130
|
-
</Carousel>
|
|
131
|
-
</Container>
|
|
132
|
-
</div>
|
|
133
|
-
);
|
|
134
82
|
return (
|
|
135
83
|
<div className="App">
|
|
136
84
|
<AppNavbar />
|
|
137
85
|
<Container>
|
|
138
86
|
<Carousel className="App-header">
|
|
139
87
|
{donateItems.map((item, index) => (
|
|
140
|
-
<Carousel.Item key={index} interval={500}>
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
</
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
88
|
+
<Carousel.Item key={index} interval={500} className="donate-carousel-item">
|
|
89
|
+
<img className="d-block w-100 donate-carousel-bg" src={red} alt={item.alt} />
|
|
90
|
+
|
|
91
|
+
<Carousel.Caption className="donate-carousel-caption">
|
|
92
|
+
<h1 className="donate-title mb-3">
|
|
93
|
+
<span style={{ textDecoration: 'underline' }}>{item.name}</span>
|
|
94
|
+
</h1>
|
|
95
|
+
|
|
96
|
+
<p className="donate-phone">
|
|
97
|
+
<b>Contact & Help by Phone: </b>
|
|
98
|
+
<span dangerouslySetInnerHTML={{ __html: item.phone }} />
|
|
99
|
+
</p>
|
|
100
|
+
|
|
101
|
+
<p className="donate-link">
|
|
102
|
+
<b>Home page: </b>
|
|
103
|
+
<a
|
|
104
|
+
href={item.link}
|
|
105
|
+
target="_blank"
|
|
106
|
+
rel="noreferrer"
|
|
107
|
+
onClick={() => trackClickOut('charity', item.link)}
|
|
108
|
+
>
|
|
109
|
+
{item.link}
|
|
110
|
+
</a>
|
|
111
|
+
</p>
|
|
112
|
+
|
|
113
|
+
<p className="overview-text donate-overview">
|
|
114
|
+
<b>Overview:</b> Founded in {item.founded}, {item.overview}
|
|
115
|
+
</p>
|
|
116
|
+
|
|
117
|
+
<p className="donate-action">
|
|
118
|
+
<a
|
|
119
|
+
href={item.donate}
|
|
120
|
+
target="_blank"
|
|
121
|
+
rel="noreferrer"
|
|
122
|
+
onClick={() => trackClickOut('charity', item.donate)}
|
|
123
|
+
>
|
|
124
|
+
Donate here
|
|
125
|
+
</a>
|
|
126
|
+
</p>
|
|
127
|
+
</Carousel.Caption>
|
|
158
128
|
</Carousel.Item>
|
|
159
129
|
))}
|
|
160
130
|
</Carousel>
|
|
@@ -61,4 +61,22 @@ describe('CoffeeCarousel', () => {
|
|
|
61
61
|
// Ensure ingredients are NOT rendered
|
|
62
62
|
expect(screen.queryByText('Ingredients:')).not.toBeInTheDocument();
|
|
63
63
|
});
|
|
64
|
+
|
|
65
|
+
test('translates localized brew titles and ingredients', () => {
|
|
66
|
+
const localizedItems = [
|
|
67
|
+
{
|
|
68
|
+
image: 'https://image1.jpg',
|
|
69
|
+
title: 'Svart Te',
|
|
70
|
+
ingredients: ' Te , Unknown ingredient , Honung ',
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
render(<CoffeeCarousel items={localizedItems} />);
|
|
75
|
+
|
|
76
|
+
expect(screen.getByText('Black Tea')).toBeInTheDocument();
|
|
77
|
+
expect(screen.getByAltText('Black Tea')).toBeInTheDocument();
|
|
78
|
+
expect(screen.getByText('Tea, Unknown ingredient, Honey')).toBeInTheDocument();
|
|
79
|
+
expect(screen.queryByText('Svart Te')).not.toBeInTheDocument();
|
|
80
|
+
expect(screen.queryByText('Te , Unknown ingredient , Honung')).not.toBeInTheDocument();
|
|
81
|
+
});
|
|
64
82
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { screen, render, waitFor } from '@testing-library/react';
|
|
1
|
+
import { screen, render, waitFor, act } from '@testing-library/react';
|
|
2
2
|
import axios from 'axios';
|
|
3
3
|
import App from '../../App';
|
|
4
4
|
import CoffeeHome from '../CoffeeHome';
|
|
@@ -25,12 +25,16 @@ describe('CoffeeHome', () => {
|
|
|
25
25
|
axios.get.mockResolvedValueOnce({ data: coffeeItems });
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
test('renders AppNavbar component', () => {
|
|
29
|
-
|
|
28
|
+
test('renders AppNavbar component', async () => {
|
|
29
|
+
await act(async () => {
|
|
30
|
+
render(<App />);
|
|
31
|
+
});
|
|
30
32
|
expect(screen.getByRole('banner')).toBeInTheDocument();
|
|
31
33
|
});
|
|
32
34
|
|
|
33
35
|
test('displays loading state initially', () => {
|
|
36
|
+
axios.get.mockReset();
|
|
37
|
+
axios.get.mockImplementation(() => new Promise(() => {}));
|
|
34
38
|
render(<CoffeeHome />);
|
|
35
39
|
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
36
40
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
1
|
+
import { render, screen, waitFor, act } from '@testing-library/react';
|
|
2
2
|
import App from '../../App';
|
|
3
3
|
import { Router, MemoryRouter } from 'react-router';
|
|
4
4
|
import ControlledCarousel from '../ControlledCarousel';
|
|
@@ -35,12 +35,16 @@ describe('Portfolio Controlled Carousel', () => {
|
|
|
35
35
|
jest.restoreAllMocks();
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
test('renders AppNavbar component', () => {
|
|
39
|
-
|
|
38
|
+
test('renders AppNavbar component', async () => {
|
|
39
|
+
await act(async () => {
|
|
40
|
+
render(<App />);
|
|
41
|
+
});
|
|
40
42
|
expect(screen.getByRole('banner')).toBeInTheDocument();
|
|
41
43
|
});
|
|
42
44
|
|
|
43
45
|
test('renders loading state initially', () => {
|
|
46
|
+
global.fetch.mockReset();
|
|
47
|
+
global.fetch.mockImplementation(() => new Promise(() => {}));
|
|
44
48
|
render(<ControlledCarousel />);
|
|
45
49
|
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
46
50
|
});
|
|
@@ -29,13 +29,28 @@ describe('RepoDetails Component', () => {
|
|
|
29
29
|
delete global.fetch;
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
-
test('renders loading spinner initially', () => {
|
|
32
|
+
test('renders loading spinner initially', async () => {
|
|
33
|
+
let resolveFetch;
|
|
34
|
+
global.fetch = jest.fn(
|
|
35
|
+
() =>
|
|
36
|
+
new Promise((resolve) => {
|
|
37
|
+
resolveFetch = resolve;
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
|
|
33
41
|
render(
|
|
34
42
|
<MemoryRouter>
|
|
35
43
|
<RepoDetails />
|
|
36
44
|
</MemoryRouter>
|
|
37
45
|
);
|
|
46
|
+
|
|
38
47
|
expect(screen.getByText(/loading/i)).toBeInTheDocument();
|
|
48
|
+
|
|
49
|
+
resolveFetch({
|
|
50
|
+
json: () => Promise.resolve([]),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await waitFor(() => expect(screen.queryByText(/loading/i)).not.toBeInTheDocument());
|
|
39
54
|
});
|
|
40
55
|
|
|
41
56
|
test('fetches and displays repo details isConor=True', async () => {
|
|
@@ -60,10 +75,11 @@ describe('RepoDetails Component', () => {
|
|
|
60
75
|
<RepoDetails />
|
|
61
76
|
</MemoryRouter>
|
|
62
77
|
);
|
|
63
|
-
await waitFor(() =>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
78
|
+
await waitFor(() => {
|
|
79
|
+
expect(screen.getByText('conorheffron/ironoc')).toBeInTheDocument();
|
|
80
|
+
expect(screen.getByText('Ironoc framework')).toBeInTheDocument();
|
|
81
|
+
expect(screen.getByText('nodejs, serverless')).toBeInTheDocument();
|
|
82
|
+
});
|
|
67
83
|
|
|
68
84
|
expect(screen.queryByRole('columnheader', { name: /Issues Count/i })).toBeInTheDocument();
|
|
69
85
|
expect(screen.getByText('37')).toBeInTheDocument();
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
3
|
import RepoIssues from '../RepoIssues';
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
// Track useMaterialReactTable call opts for assertions (must start with 'mock' for Jest hoisting)
|
|
6
|
+
const mockUseMRTOpts = [];
|
|
5
7
|
|
|
6
8
|
// Mock react-router
|
|
7
9
|
jest.mock('react-router', () => ({
|
|
@@ -27,16 +29,31 @@ jest.mock('react-bootstrap', () => ({
|
|
|
27
29
|
jest.mock('../../AppNavbar', () => () => <div data-testid="navbar">Navbar</div>);
|
|
28
30
|
jest.mock('../../LoadingSpinner', () => () => <div data-testid="spinner">Loading...</div>);
|
|
29
31
|
|
|
30
|
-
// Mock MaterialReactTable and useMaterialReactTable
|
|
32
|
+
// Mock MaterialReactTable and useMaterialReactTable.
|
|
33
|
+
// useMaterialReactTable is a plain function (not jest.fn) so React 19 concurrent mode
|
|
34
|
+
// commits re-renders correctly. Calls are tracked via mockUseMRTOpts for assertions.
|
|
35
|
+
// columnFilters from initialState are applied to table.data so MaterialReactTable
|
|
36
|
+
// only receives the filtered rows, allowing DOM-level assertions on visibility.
|
|
31
37
|
jest.mock('material-react-table', () => ({
|
|
32
38
|
MaterialReactTable: ({ table }) => (
|
|
33
39
|
<div data-testid="mrt-table">
|
|
34
|
-
{table
|
|
40
|
+
{(table?.data ?? []).map((issue, idx) => (
|
|
35
41
|
<div key={idx} data-testid="mrt-row">{issue.title}</div>
|
|
36
42
|
))}
|
|
37
43
|
</div>
|
|
38
44
|
),
|
|
39
|
-
useMaterialReactTable:
|
|
45
|
+
useMaterialReactTable: (opts) => {
|
|
46
|
+
mockUseMRTOpts.push(opts);
|
|
47
|
+
const filters = opts?.initialState?.columnFilters ?? [];
|
|
48
|
+
let data = opts?.data ?? [];
|
|
49
|
+
filters.forEach((filter) => {
|
|
50
|
+
data = data.filter((row) => {
|
|
51
|
+
const val = row[filter.id];
|
|
52
|
+
return Array.isArray(filter.value) ? filter.value.includes(val) : val === filter.value;
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
return { ...opts, data };
|
|
56
|
+
},
|
|
40
57
|
}));
|
|
41
58
|
|
|
42
59
|
// Mock @mui/material theme functions
|
|
@@ -58,6 +75,7 @@ describe('RepoIssues', () => {
|
|
|
58
75
|
const mockIssuesResponse = [];
|
|
59
76
|
|
|
60
77
|
beforeEach(() => {
|
|
78
|
+
mockUseMRTOpts.length = 0;
|
|
61
79
|
jest.clearAllMocks();
|
|
62
80
|
useNavigate.mockReturnValue(mockNavigate);
|
|
63
81
|
global.fetch = jest.fn(() =>
|
|
@@ -98,19 +116,21 @@ describe('RepoIssues', () => {
|
|
|
98
116
|
expect(screen.queryByTestId('spinner')).not.toBeInTheDocument()
|
|
99
117
|
);
|
|
100
118
|
expect(screen.getByTestId('mrt-table')).toBeInTheDocument();
|
|
119
|
+
expect(screen.getByText('Test Issue')).toBeInTheDocument();
|
|
101
120
|
});
|
|
102
121
|
|
|
103
|
-
it('
|
|
122
|
+
it('defaults to open issues while hiding state and description columns', async () => {
|
|
104
123
|
useParams.mockReturnValue({ id: 'user', repo: 'repo' });
|
|
105
124
|
render(<RepoIssues />);
|
|
106
125
|
await waitFor(() =>
|
|
107
126
|
expect(screen.queryByTestId('spinner')).not.toBeInTheDocument()
|
|
108
127
|
);
|
|
109
128
|
|
|
110
|
-
expect(
|
|
129
|
+
expect(mockUseMRTOpts).toContainEqual(
|
|
111
130
|
expect.objectContaining({
|
|
112
131
|
initialState: expect.objectContaining({
|
|
113
132
|
showColumnFilters: true,
|
|
133
|
+
columnFilters: [{ id: 'state', value: ['open'] }],
|
|
114
134
|
columnVisibility: {
|
|
115
135
|
state: false,
|
|
116
136
|
body: false,
|
|
@@ -120,6 +140,24 @@ describe('RepoIssues', () => {
|
|
|
120
140
|
);
|
|
121
141
|
});
|
|
122
142
|
|
|
143
|
+
it('renders open issues and excludes closed issues by default', async () => {
|
|
144
|
+
useParams.mockReturnValue({ id: 'user', repo: 'repo' });
|
|
145
|
+
global.fetch = jest.fn(() =>
|
|
146
|
+
Promise.resolve({
|
|
147
|
+
json: () => Promise.resolve([
|
|
148
|
+
{ number: 1, state: 'open', labels: [], title: 'Open Issue', body: '' },
|
|
149
|
+
{ number: 2, state: 'closed', labels: [], title: 'Closed Issue', body: '' },
|
|
150
|
+
]),
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
window.fetch = global.fetch;
|
|
154
|
+
render(<RepoIssues />);
|
|
155
|
+
await waitFor(() =>
|
|
156
|
+
expect(screen.getByText('Open Issue')).toBeInTheDocument()
|
|
157
|
+
);
|
|
158
|
+
expect(screen.queryByText('Closed Issue')).not.toBeInTheDocument();
|
|
159
|
+
});
|
|
160
|
+
|
|
123
161
|
it('navigates on form submit', async () => {
|
|
124
162
|
useParams.mockReturnValue({ id: 'user', repo: 'repo' });
|
|
125
163
|
global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve([]) }));
|