import OpenWeatherMap from 'openweathermap-ts'; import { describe, expect, it, vi, beforeEach, type Mocked } from 'vitest'; import { Weather } from '../src/weather.js'; import type { CurrentResponse, ThreeHourResponse } from '../src/weather.js'; // #region mock API responses const current: CurrentResponse = { "coord": { "lon": 7.367, "lat": 45.133 }, "weather": [ { "id": 501, "main": "Rain", "description": "moderate rain", "icon": "10d" } ], "base": "stations", "main": { "temp": 284.2, "feels_like": 282.93, "temp_min": 283.06, "temp_max": 286.82, "pressure": 1021, "humidity": 60, "sea_level": 1021, "grnd_level": 910 }, "visibility": 10000, "wind": { "speed": 4.09, "deg": 121, "gust": 3.47 }, "rain": { "1h": 2.73 }, "clouds": { "all": 83 }, "dt": 1726660758, "sys": { "type": 1, "id": 6736, "country": "IT", "sunrise": 1726636384, "sunset": 1726680975 }, "timezone": 7200, "id": 3165523, "name": "Province of Turin", "cod": 200 }; const threeHour: ThreeHourResponse = { "cod": "200", "message": 0, "cnt": 96, "list": [ { "dt": 1661875200, "main": { "temp": 296.34, "temp_min": 296.34, "temp_max": 298.24, "pressure": 1015, "sea_level": 1015, "grnd_level": 933, "humidity": 50, "temp_kf": -1.9 }, "weather": [ { "id": 500, "main": "Rain", "description": "light rain", "icon": "10d" } ], "clouds": { "all": 97 }, "wind": { "speed": 1.06, "deg": 66 }, "rain": { "3h": 1 }, "sys": { "pod": "d" }, "dt_txt": "2022-08-30 16:00:00" }, { "dt": 1661878800, "main": { "temp": 296.31, "temp_min": 296.2, "temp_max": 296.31, "pressure": 1015, "sea_level": 1015, "grnd_level": 932, "humidity": 53, "temp_kf": 0.11 }, "weather": [ { "id": 500, "main": "Rain", "description": "light rain", "icon": "10d" } ], "clouds": { "all": 95 }, "wind": { "speed": 1.58, "deg": 103 }, "rain": { "3h": 0.24 }, "sys": { "pod": "d" }, "dt_txt": "2022-08-30 17:00:00" }, { "dt": 1661882400, "main": { "temp": 294.94, "temp_min": 292.84, "temp_max": 294.94, "pressure": 1015, "sea_level": 1015, "grnd_level": 931, "humidity": 60, "temp_kf": 2.1 }, "weather": [ { "id": 500, "main": "Rain", "description": "light rain", "icon": "10n" } ], "clouds": { "all": 93 }, "wind": { "speed": 1.97, "deg": 157 }, "rain": { "3h": 0.2 }, "sys": { "pod": "n" }, "dt_txt": "2022-08-30 18:00:00" }, { "dt": 1662217200, "main": { "temp": 294.14, "temp_min": 294.14, "temp_max": 294.14, "pressure": 1014, "sea_level": 1014, "grnd_level": 931, "humidity": 65, "temp_kf": 0 }, "weather": [ { "id": 804, "main": "Clouds", "description": "overcast clouds", "icon": "04d" } ], "clouds": { "all": 100 }, "wind": { "speed": 0.91, "deg": 104 }, "sys": { "pod": "d" }, "dt_txt": "2022-09-03 15:00:00" } ], "city": { "id": 3163858, "name": "Zocca", "coord": { "lat": 44.34, "lon": 10.99 }, "country": "IT" } } // #endregion vi.mock('openweathermap-ts', () => { return { default: { default: vi.fn().mockImplementation((_) => { return { setCityName: vi.fn(() => undefined), setCityId: vi.fn(() => undefined), setZipCode: vi.fn(() => undefined), setGeoCoordinates: vi.fn(() => undefined), getCurrentWeatherByCityName: vi.fn(async () => current), getCurrentWeatherByCityId: vi.fn(async () => current), getCurrentWeatherByZipcode: vi.fn(async () => current), getCurrentWeatherByGeoCoordinates: vi.fn(async () => current), getThreeHourForecastByCityName: vi.fn(async () => threeHour), getThreeHourForecastByCityId: vi.fn(async () => threeHour), getThreeHourForecastByZipcode: vi.fn(async () => threeHour), getThreeHourForecastByGeoCoordinates: vi.fn(async () => threeHour), } }) } } }); let weather: Weather = null; describe.for( [ { weatherFactory: () => new Weather({ key: 'api-key', city: { cityName: 'Madison', state: 'Wisconsin', countryCode: 'US' } }), by: 'CityName' }, { weatherFactory: () => new Weather({ key: 'api-key', cityid: 1 }), by: 'CityId' }, { weatherFactory: () => new Weather({ key: 'api-key', zip: 53702, country: 'US' }), by: 'Zipcode' }, { weatherFactory: () => new Weather({ key: 'api-key', coordinates: [0, 0] }), by: 'GeoCoordinates' }, { weatherFactory: () => new Weather({ key: 'api-key', coordinates: '1000 , 1000' }), by: 'GeoCoordinates' } ])('weather API using city name', ({ weatherFactory, by }) => { beforeEach(() => { vi.clearAllMocks(); weather = weatherFactory(); }); it('gets current weather', async () => { expect(await weather.getCurrentWeather()).to.deep.equal(current); expect(OpenWeatherMap.default).toBeCalledWith({ apiKey: 'api-key' }); const MockedOpenWeather = vi.mocked(OpenWeatherMap.default); const openWeather = MockedOpenWeather.mock.results[0].value; expect(openWeather[`getCurrentWeatherBy${by}`]).toBeCalledTimes(1); }); it('gets the 3h forecast', async () => { expect(await weather.getThreeHourForecast()).to.deep.equal(threeHour); expect(OpenWeatherMap.default).toBeCalledWith({ apiKey: 'api-key' }); const MockedOpenWeather = vi.mocked(OpenWeatherMap.default); const openWeather = MockedOpenWeather.mock.results[0].value; expect(openWeather[`getThreeHourForecastBy${by}`]).toBeCalledTimes(1); }); it('only calls the api once', async () => { expect(await weather.getCurrentWeather()).to.deep.equal(current); expect(await weather.getCurrentWeather()).to.deep.equal(current); expect(await weather.getThreeHourForecast()).to.deep.equal(threeHour); expect(await weather.getThreeHourForecast()).to.deep.equal(threeHour); expect(OpenWeatherMap.default).toBeCalledWith({ apiKey: 'api-key' }); const MockedOpenWeather = vi.mocked(OpenWeatherMap.default); const openWeather = MockedOpenWeather.mock.results[0].value; expect(openWeather[`getCurrentWeatherBy${by}`]).toBeCalledTimes(1); expect(openWeather[`getThreeHourForecastBy${by}`]).toBeCalledTimes(1); }); } ); describe('invalid weather object', () => { beforeEach(() => { vi.clearAllMocks(); weather = new Weather({ key: 'api-key', cityid: 1 }); weather['locationType'] = null; }); it('throws an exception when getCurrentWeather is called', async () => { await expect(weather.getCurrentWeather()).rejects.toThrow(/location type/); }); it('throws an exception when getThreeHourForecast is called', async () => { await expect(weather.getThreeHourForecast()).rejects.toThrow(/location type/); }); });