diff --git a/javascriptapp/package.json b/javascriptapp/package.json index 95728f1..76ecd7e 100644 --- a/javascriptapp/package.json +++ b/javascriptapp/package.json @@ -20,6 +20,7 @@ "dependencies": { "dotenv": "^16.4.7", "express": "^4.21.2", + "jsdom": "^26.1.0", "path": "^0.12.7" } } diff --git a/javascriptapp/public/cities.json b/javascriptapp/public/cities.json new file mode 100644 index 0000000..dfd1b59 --- /dev/null +++ b/javascriptapp/public/cities.json @@ -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" +] diff --git a/javascriptapp/public/index.html b/javascriptapp/public/index.html index 6b833fd..65776bc 100644 --- a/javascriptapp/public/index.html +++ b/javascriptapp/public/index.html @@ -78,9 +78,11 @@

My Weather App

-
- - +
+ +
diff --git a/javascriptapp/public/script.js b/javascriptapp/public/script.js index 93385a1..3fc74c8 100644 --- a/javascriptapp/public/script.js +++ b/javascriptapp/public/script.js @@ -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'); @@ -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 = ""; @@ -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(); diff --git a/javascriptapp/tests/server.test.js b/javascriptapp/tests/server.test.js index f5b29f7..140a240 100644 --- a/javascriptapp/tests/server.test.js +++ b/javascriptapp/tests/server.test.js @@ -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 () => { @@ -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); @@ -20,17 +44,15 @@ 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, @@ -38,18 +60,63 @@ describe('Weather API error handling', () => { }); 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(` + + +

+ `, { 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!'); + }); +});