Skip to content

Commit dd4df77

Browse files
Add new dropdown and update testing
1 parent 00462df commit dd4df77

5 files changed

Lines changed: 125 additions & 14 deletions

File tree

javascriptapp/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"dependencies": {
2121
"dotenv": "^16.4.7",
2222
"express": "^4.21.2",
23+
"jsdom": "^26.1.0",
2324
"path": "^0.12.7"
2425
}
2526
}

javascriptapp/public/cities.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[
2+
"New York",
3+
"Los Angeles",
4+
"Chicago",
5+
"Houston",
6+
"Phoenix",
7+
"Philadelphia",
8+
"San Antonio",
9+
"San Diego",
10+
"Dallas",
11+
"San Jose",
12+
"London",
13+
"Paris",
14+
"Berlin",
15+
"Tokyo",
16+
"Sydney",
17+
"Pinehurst"
18+
]

javascriptapp/public/index.html

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,11 @@
7878
<div class="container">
7979
<h1>My Weather App</h1>
8080

81-
<div class="form-group">
82-
<input type="text" id="cityInput" placeholder="Enter a city..." />
83-
<button id="getWeatherBtn">Get Weather</button>
81+
<div class="form-group" style="display: flex; justify-content: center; align-items: center; gap: 8px;">
82+
<select id="citySelect" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc;">
83+
<option value="">Select a city...</option>
84+
</select>
85+
<button id="getWeatherBtn" style="padding: 8px 12px; border: none; border-radius: 4px; background: #007bff; color: white; cursor: pointer;">Get Weather</button>
8486
</div>
8587

8688
<div class="weather-container">

javascriptapp/public/script.js

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const getWeatherBtn = document.getElementById('getWeatherBtn');
2-
const cityInput = document.getElementById('cityInput');
2+
const citySelect = document.getElementById('citySelect');
33

44
const weatherTitle = document.getElementById('weatherTitle');
55
const weatherIcon = document.getElementById('weatherIcon');
@@ -9,14 +9,32 @@ const feelsLike = document.getElementById('feelsLike');
99
const windSpeed = document.getElementById('windSpeed');
1010
const errorMessage = document.getElementById('errorMessage');
1111

12+
async function loadCities() {
13+
try {
14+
const response = await fetch('cities.json');
15+
if (!response.ok) {
16+
throw new Error('Failed to load cities');
17+
}
18+
const cities = await response.json();
19+
cities.forEach(city => {
20+
const option = document.createElement('option');
21+
option.value = city;
22+
option.textContent = city;
23+
citySelect.appendChild(option);
24+
});
25+
} catch (error) {
26+
errorMessage.textContent = error.message;
27+
}
28+
}
29+
1230
getWeatherBtn.addEventListener('click', async () => {
13-
const city = cityInput.value.trim();
31+
const city = citySelect.value.trim();
1432
if (!city) {
15-
errorMessage.textContent = "Please enter a city!";
33+
errorMessage.textContent = "Please select a city!";
1634
return;
1735
}
1836

19-
// Clear previous content
37+
// Clear previous content and error message
2038
weatherTitle.textContent = "";
2139
weatherIcon.src = "";
2240
weatherDescription.textContent = "";
@@ -49,7 +67,12 @@ getWeatherBtn.addEventListener('click', async () => {
4967
weatherIcon.alt = data.description || "Weather Icon";
5068
weatherIcon.style.display = "inline-block";
5169
}
70+
71+
// Clear error message on successful fetch
72+
errorMessage.textContent = "";
5273
} catch (error) {
5374
errorMessage.textContent = error.message;
5475
}
5576
});
77+
78+
loadCities();

javascriptapp/tests/server.test.js

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
const request = require('supertest');
22
const app = require('../server');
3+
const { JSDOM } = require('jsdom');
4+
const fs = require('fs');
5+
const path = require('path');
36

47
describe('Weather API', () => {
58
it('returns weather data for a valid city', async () => {
@@ -8,10 +11,31 @@ describe('Weather API', () => {
811

912
expect(response.statusCode).toBe(200);
1013
expect(response.body).toHaveProperty('name', 'London');
14+
// Branch coverage for description line 31: test with description present
1115
expect(response.body).toHaveProperty('description');
16+
expect(typeof response.body.description).toBe('string');
17+
expect(response.body.description.length).toBeGreaterThan(0);
1218
expect(response.body).toHaveProperty('temp');
1319
});
1420

21+
it('returns weather data with empty description', async () => {
22+
// Mock fetch to return data with no description
23+
jest.spyOn(global, 'fetch').mockResolvedValueOnce({
24+
ok: true,
25+
json: jest.fn().mockResolvedValue({
26+
name: 'TestCity',
27+
weather: [{}],
28+
main: { temp: 70, feels_like: 65 },
29+
wind: { speed: 5 }
30+
}),
31+
});
32+
33+
const response = await request(app).get('/api/weather?city=TestCity');
34+
expect(response.statusCode).toBe(200);
35+
expect(response.body).toHaveProperty('description', '');
36+
global.fetch.mockRestore();
37+
});
38+
1539
it('returns 400 if city is missing', async () => {
1640
const response = await request(app).get('/api/weather');
1741
expect(response.statusCode).toBe(400);
@@ -20,36 +44,79 @@ describe('Weather API', () => {
2044
});
2145

2246
describe('Weather API error handling', () => {
23-
// Before each test, spy on the global fetch (Node 18+) or require('node-fetch') if using that
2447
beforeEach(() => {
25-
jest.spyOn(global, 'fetch'); // if on Node 18+, 'global.fetch' is the built-in
48+
jest.spyOn(global, 'fetch');
2649
});
2750

2851
afterEach(() => {
29-
global.fetch.mockRestore(); // restore original fetch
52+
global.fetch.mockRestore();
3053
});
3154

3255
it('handles non-OK response from OpenWeather', async () => {
33-
// Force fetch to return { ok: false, status: 404, ... }
3456
global.fetch.mockResolvedValue({
3557
ok: false,
3658
status: 404,
3759
json: jest.fn().mockResolvedValue({}),
3860
});
3961

4062
const response = await request(app).get('/api/weather?city=FakeCity');
41-
// The code sets status to response.status => 404 in this case
4263
expect(response.statusCode).toBe(404);
4364
expect(response.body).toHaveProperty('error', 'Failed to fetch weather data');
4465
});
4566

4667
it('handles fetch throw (network error, etc.)', async () => {
47-
// Force fetch to throw an error
4868
global.fetch.mockRejectedValue(new Error('Network error'));
4969

5070
const response = await request(app).get('/api/weather?city=FakeCity2');
51-
// The code should catch and return status 500, { error: 'Server error' }
5271
expect(response.statusCode).toBe(500);
5372
expect(response.body).toHaveProperty('error', 'Server error');
5473
});
5574
});
75+
76+
describe('Frontend script', () => {
77+
let window;
78+
let document;
79+
80+
beforeEach(() => {
81+
const dom = new JSDOM(`
82+
<select id="citySelect">
83+
<option value="">Select a city...</option>
84+
</select>
85+
<button id="getWeatherBtn">Get Weather</button>
86+
<p id="errorMessage" class="error"></p>
87+
`, { runScripts: "dangerously", resources: "usable" });
88+
89+
window = dom.window;
90+
document = window.document;
91+
92+
global.document = document;
93+
global.window = window;
94+
95+
global.fetch = jest.fn(() =>
96+
Promise.resolve({
97+
ok: true,
98+
json: () => Promise.resolve(['City1', 'City2']),
99+
})
100+
);
101+
102+
const scriptContent = fs.readFileSync(path.resolve(__dirname, '../public/script.js'), 'utf-8');
103+
const scriptEl = document.createElement('script');
104+
scriptEl.textContent = scriptContent;
105+
document.body.appendChild(scriptEl);
106+
});
107+
108+
afterEach(() => {
109+
delete global.document;
110+
delete global.window;
111+
delete global.fetch;
112+
});
113+
114+
it('shows error if no city selected', () => {
115+
const getWeatherBtn = document.getElementById('getWeatherBtn');
116+
const errorMessage = document.getElementById('errorMessage');
117+
118+
getWeatherBtn.click();
119+
120+
expect(errorMessage.textContent).toBe('Please select a city!');
121+
});
122+
});

0 commit comments

Comments
 (0)