Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions javascriptapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"dependencies": {
"dotenv": "^16.4.7",
"express": "^4.21.2",
"jsdom": "^26.1.0",
"path": "^0.12.7"
}
}
18 changes: 18 additions & 0 deletions javascriptapp/public/cities.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
"New York",
"Los Angeles",
"Chicago",
"Houston",
"Phoenix",
"Philadelphia",
"San Antonio",
"San Diego",
"Dallas",
"San Jose",
"London",
"Paris",
"Berlin",
"Tokyo",
"Sydney",
"Pinehurst"
]
8 changes: 5 additions & 3 deletions javascriptapp/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,11 @@
<div class="container">
<h1>My Weather App</h1>

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

<div class="weather-container">
Expand Down
31 changes: 27 additions & 4 deletions javascriptapp/public/script.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const getWeatherBtn = document.getElementById('getWeatherBtn');
const cityInput = document.getElementById('cityInput');
const citySelect = document.getElementById('citySelect');

const weatherTitle = document.getElementById('weatherTitle');
const weatherIcon = document.getElementById('weatherIcon');
Expand All @@ -9,14 +9,32 @@ const feelsLike = document.getElementById('feelsLike');
const windSpeed = document.getElementById('windSpeed');
const errorMessage = document.getElementById('errorMessage');

async function loadCities() {
try {
const response = await fetch('cities.json');
if (!response.ok) {
throw new Error('Failed to load cities');
}
const cities = await response.json();
cities.forEach(city => {
const option = document.createElement('option');
option.value = city;
option.textContent = city;
citySelect.appendChild(option);
});
} catch (error) {
errorMessage.textContent = error.message;
}
}

getWeatherBtn.addEventListener('click', async () => {
const city = cityInput.value.trim();
const city = citySelect.value.trim();
if (!city) {
errorMessage.textContent = "Please enter a city!";
errorMessage.textContent = "Please select a city!";
return;
}

// Clear previous content
// Clear previous content and error message
weatherTitle.textContent = "";
weatherIcon.src = "";
weatherDescription.textContent = "";
Expand Down Expand Up @@ -49,7 +67,12 @@ getWeatherBtn.addEventListener('click', async () => {
weatherIcon.alt = data.description || "Weather Icon";
weatherIcon.style.display = "inline-block";
}

// Clear error message on successful fetch
errorMessage.textContent = "";
} catch (error) {
errorMessage.textContent = error.message;
}
});

loadCities();
81 changes: 74 additions & 7 deletions javascriptapp/tests/server.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
const request = require('supertest');
const app = require('../server');
const { JSDOM } = require('jsdom');
const fs = require('fs');
const path = require('path');

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

expect(response.statusCode).toBe(200);
expect(response.body).toHaveProperty('name', 'London');
// Branch coverage for description line 31: test with description present
expect(response.body).toHaveProperty('description');
expect(typeof response.body.description).toBe('string');
expect(response.body.description.length).toBeGreaterThan(0);
expect(response.body).toHaveProperty('temp');
});

it('returns weather data with empty description', async () => {
// Mock fetch to return data with no description
jest.spyOn(global, 'fetch').mockResolvedValueOnce({
ok: true,
json: jest.fn().mockResolvedValue({
name: 'TestCity',
weather: [{}],
main: { temp: 70, feels_like: 65 },
wind: { speed: 5 }
}),
});

const response = await request(app).get('/api/weather?city=TestCity');
expect(response.statusCode).toBe(200);
expect(response.body).toHaveProperty('description', '');
global.fetch.mockRestore();
});

it('returns 400 if city is missing', async () => {
const response = await request(app).get('/api/weather');
expect(response.statusCode).toBe(400);
Expand All @@ -20,36 +44,79 @@ describe('Weather API', () => {
});

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

afterEach(() => {
global.fetch.mockRestore(); // restore original fetch
global.fetch.mockRestore();
});

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

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

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

const response = await request(app).get('/api/weather?city=FakeCity2');
// The code should catch and return status 500, { error: 'Server error' }
expect(response.statusCode).toBe(500);
expect(response.body).toHaveProperty('error', 'Server error');
});
});

describe('Frontend script', () => {
let window;
let document;

beforeEach(() => {
const dom = new JSDOM(`
<select id="citySelect">
<option value="">Select a city...</option>
</select>
<button id="getWeatherBtn">Get Weather</button>
<p id="errorMessage" class="error"></p>
`, { runScripts: "dangerously", resources: "usable" });

window = dom.window;
document = window.document;

global.document = document;
global.window = window;

global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(['City1', 'City2']),
})
);

const scriptContent = fs.readFileSync(path.resolve(__dirname, '../public/script.js'), 'utf-8');
const scriptEl = document.createElement('script');
scriptEl.textContent = scriptContent;
document.body.appendChild(scriptEl);
});

afterEach(() => {
delete global.document;
delete global.window;
delete global.fetch;
});

it('shows error if no city selected', () => {
const getWeatherBtn = document.getElementById('getWeatherBtn');
const errorMessage = document.getElementById('errorMessage');

getWeatherBtn.click();

expect(errorMessage.textContent).toBe('Please select a city!');
});
});