@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 CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  The ironoc UI built with react for personal portfolio website.
4
4
 
5
+ [![Deploy to Amazon ECS](https://github.com/conorheffron/ironoc/actions/workflows/aws.yml/badge.svg)](https://github.com/conorheffron/ironoc/actions/workflows/aws.yml)
6
+
5
7
  [![Node.js Package](https://github.com/conorheffron/ironoc/actions/workflows/npm-publish-packages.yml/badge.svg)](https://github.com/conorheffron/ironoc/actions/workflows/npm-publish-packages.yml)
6
8
 
7
9
  [![Node.js CI](https://github.com/conorheffron/ironoc/actions/workflows/node.js.yml/badge.svg)](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.7",
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": "^11.14.0",
10
- "@emotion/styled": "^11.14.1",
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
- top: 0;
54
- bottom: auto;
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
- padding-left: 15%;
64
- padding-right: 15%;
64
+ padding-left: 15%;
65
+ padding-right: 15%;
65
66
  }
66
67
 
67
68
  .strava-badge {
68
- display: inline-block;
69
- background-color: #FC5200;
70
- color: #fff;
71
- padding: 5px 10px 5px 30px;
72
- font-size: 11px;
73
- font-family: Helvetica, Arial, sans-serif;
74
- white-space: nowrap;
75
- text-decoration: none;
76
- background-repeat: no-repeat;
77
- background-position: 10px center;
78
- border-radius: 3px;
79
- background-image: url('https://badges.strava.com/logo-strava-echelon.png')
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', // Replace with your GraphQL endpoint
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
- render(<Footer />);
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={item.title} />
80
+ <img src={item.image} alt={translatedTitle} />
27
81
  <Carousel.Caption>
28
- <h3>{item.title}</h3>
82
+ <h3>{translatedTitle}</h3>
29
83
  {item.ingredients && item.ingredients.length > 0 && (
30
84
  <h5>
31
85
  <b>Ingredients:</b>{' '}
32
- {Array.isArray(item.ingredients)
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
  }
@@ -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
- <a href={item.donate} target="_blank" rel="noreferrer">
142
- <img className="d-block w-100" src={red} alt={item.alt} />
143
- <Carousel.Caption>
144
- <h1>
145
- <u>{item.name}</u>
146
- </h1><br />
147
- <h8>
148
- <b>Contact & Help by Phone: </b><span dangerouslySetInnerHTML={{ __html: item.phone }} />
149
- </h8><br />
150
- <h7>
151
- <b>Home page: </b><a href={item.link} target="_blank" rel="noreferrer">{item.link}</a>
152
- </h7><br /><br />
153
- <h11 className="overview-text">
154
- <b>Overview:</b> Founded in {item.founded}, {item.overview}
155
- </h11>
156
- </Carousel.Caption>
157
- </a>
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 [value, setValue] = useState('');
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 handleChange = (event) => setValue(event.target.value);
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
- navigate(`/issues/${id}/${value}`, {
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: id,
60
- repo: value,
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={value}
192
- onChange={handleChange}
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
- render(<App />);
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
- render(<App />);
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(() => expect(fetch).toHaveBeenCalledTimes(1));
64
- expect(screen.getByText('conorheffron/ironoc')).toBeInTheDocument();
65
- expect(screen.getByText('Ironoc framework')).toBeInTheDocument();
66
- expect(screen.getByText('nodejs, serverless')).toBeInTheDocument();
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 input = screen.getByTestId('form-control');
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(input, { target: { value: 'newrepo' } });
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
  });