@conorheffron/ironoc-frontend 9.1.7 → 9.1.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/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 +22 -8
- 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 +28 -4
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.9",
|
|
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>
|
|
@@ -35,7 +35,8 @@ const RepoIssues = () => {
|
|
|
35
35
|
|
|
36
36
|
const [repoIssueList, setRepoIssueList] = useState([]);
|
|
37
37
|
const [isLoading, setIsLoading] = useState(true);
|
|
38
|
-
const [
|
|
38
|
+
const [usernameValue, setUsernameValue] = useState(id);
|
|
39
|
+
const [repoValue, setRepoValue] = useState(repo);
|
|
39
40
|
|
|
40
41
|
useEffect(() => {
|
|
41
42
|
const fetchIssues = async () => {
|
|
@@ -44,20 +45,27 @@ const RepoIssues = () => {
|
|
|
44
45
|
const body = await response.json();
|
|
45
46
|
setRepoIssueList(body);
|
|
46
47
|
}
|
|
48
|
+
setUsernameValue(id);
|
|
49
|
+
setRepoValue(repo);
|
|
47
50
|
setIsLoading(false);
|
|
48
51
|
};
|
|
49
52
|
fetchIssues();
|
|
50
53
|
}, [id, repo]);
|
|
51
54
|
|
|
52
|
-
const
|
|
55
|
+
const handleUsernameChange = (event) => setUsernameValue(event.target.value);
|
|
56
|
+
|
|
57
|
+
const handleRepoChange = (event) => setRepoValue(event.target.value);
|
|
53
58
|
|
|
54
59
|
const onSubmit = (event) => {
|
|
55
60
|
event.preventDefault();
|
|
56
|
-
|
|
61
|
+
const searchUsername = usernameValue.trim() || id;
|
|
62
|
+
const searchRepo = repoValue.trim() || repo;
|
|
63
|
+
|
|
64
|
+
navigate(`/issues/${searchUsername}/${searchRepo}`, {
|
|
57
65
|
replace: true,
|
|
58
66
|
state: {
|
|
59
|
-
id:
|
|
60
|
-
repo:
|
|
67
|
+
id: searchUsername,
|
|
68
|
+
repo: searchRepo,
|
|
61
69
|
},
|
|
62
70
|
});
|
|
63
71
|
navigate(0);
|
|
@@ -183,13 +191,19 @@ const RepoIssues = () => {
|
|
|
183
191
|
<Container fluid={true}>
|
|
184
192
|
<br />
|
|
185
193
|
<InputGroup className="mb-3">
|
|
194
|
+
<Form.Control
|
|
195
|
+
placeholder="Enter GitHub User ID... Example: conorheffron"
|
|
196
|
+
aria-label="Enter GitHub User ID..."
|
|
197
|
+
type="text"
|
|
198
|
+
value={usernameValue}
|
|
199
|
+
onChange={handleUsernameChange}
|
|
200
|
+
/>
|
|
186
201
|
<Form.Control
|
|
187
202
|
placeholder="Enter Project Name... Example: ironoc-db"
|
|
188
203
|
aria-label="Enter Project Name..."
|
|
189
|
-
aria-describedby="basic-addon2"
|
|
190
204
|
type="text"
|
|
191
|
-
value={
|
|
192
|
-
onChange={
|
|
205
|
+
value={repoValue}
|
|
206
|
+
onChange={handleRepoChange}
|
|
193
207
|
/>
|
|
194
208
|
<Button
|
|
195
209
|
variant="outline-secondary"
|
|
@@ -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();
|
|
@@ -164,17 +164,41 @@ describe('RepoIssues', () => {
|
|
|
164
164
|
window.fetch = global.fetch;
|
|
165
165
|
render(<RepoIssues />);
|
|
166
166
|
await waitFor(() => expect(screen.queryByTestId('spinner')).not.toBeInTheDocument());
|
|
167
|
-
const
|
|
167
|
+
const usernameInput = screen.getByRole('textbox', { name: /Enter GitHub User ID/i });
|
|
168
|
+
const repoInput = screen.getByRole('textbox', { name: /Enter Project Name/i });
|
|
168
169
|
const button = screen.getByText(/Search Issues/i);
|
|
169
|
-
fireEvent.change(
|
|
170
|
+
fireEvent.change(usernameInput, { target: { value: 'other-user' } });
|
|
171
|
+
fireEvent.change(repoInput, { target: { value: 'newrepo' } });
|
|
170
172
|
fireEvent.click(button);
|
|
171
|
-
expect(mockNavigate).toHaveBeenCalledWith('/issues/user/newrepo', {
|
|
173
|
+
expect(mockNavigate).toHaveBeenCalledWith('/issues/other-user/newrepo', {
|
|
172
174
|
replace: true,
|
|
173
175
|
state: {
|
|
174
|
-
id: 'user',
|
|
176
|
+
id: 'other-user',
|
|
175
177
|
repo: 'newrepo',
|
|
176
178
|
},
|
|
177
179
|
});
|
|
178
180
|
expect(mockNavigate).toHaveBeenCalledWith(0);
|
|
179
181
|
});
|
|
182
|
+
|
|
183
|
+
it('uses current route values when search fields are blank', async () => {
|
|
184
|
+
useParams.mockReturnValue({ id: 'user', repo: 'repo' });
|
|
185
|
+
global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve([]) }));
|
|
186
|
+
window.fetch = global.fetch;
|
|
187
|
+
render(<RepoIssues />);
|
|
188
|
+
await waitFor(() => expect(screen.queryByTestId('spinner')).not.toBeInTheDocument());
|
|
189
|
+
const usernameInput = screen.getByRole('textbox', { name: /Enter GitHub User ID/i });
|
|
190
|
+
const repoInput = screen.getByRole('textbox', { name: /Enter Project Name/i });
|
|
191
|
+
const button = screen.getByText(/Search Issues/i);
|
|
192
|
+
fireEvent.change(usernameInput, { target: { value: '' } });
|
|
193
|
+
fireEvent.change(repoInput, { target: { value: '' } });
|
|
194
|
+
fireEvent.click(button);
|
|
195
|
+
expect(mockNavigate).toHaveBeenCalledWith('/issues/user/repo', {
|
|
196
|
+
replace: true,
|
|
197
|
+
state: {
|
|
198
|
+
id: 'user',
|
|
199
|
+
repo: 'repo',
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
expect(mockNavigate).toHaveBeenCalledWith(0);
|
|
203
|
+
});
|
|
180
204
|
});
|