Procedurally generates a radio weather report

switch out weather library

+6 -51
package-lock.json
··· 10 "license": "MIT", 11 "dependencies": { 12 "json5": "2.2.3", 13 - "openweathermap-ts": "1.2.10" 14 }, 15 "devDependencies": { 16 "@types/node": "24.3.0", ··· 1622 "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1623 } 1624 }, 1625 - "node_modules/node-fetch": { 1626 - "version": "2.7.0", 1627 - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", 1628 - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", 1629 - "license": "MIT", 1630 - "dependencies": { 1631 - "whatwg-url": "^5.0.0" 1632 - }, 1633 - "engines": { 1634 - "node": "4.x || >=6.0.0" 1635 - }, 1636 - "peerDependencies": { 1637 - "encoding": "^0.1.0" 1638 - }, 1639 - "peerDependenciesMeta": { 1640 - "encoding": { 1641 - "optional": true 1642 - } 1643 - } 1644 - }, 1645 - "node_modules/openweathermap-ts": { 1646 - "version": "1.2.10", 1647 - "resolved": "https://registry.npmjs.org/openweathermap-ts/-/openweathermap-ts-1.2.10.tgz", 1648 - "integrity": "sha512-Zckv2aXN8ENSeAeroces2jJciLWb6aLNXEmvG6pmF+BcIMw2kwRo6++/AKUNoU5suOp47UWA6lllDV0TNm//OA==", 1649 - "license": "MIT", 1650 - "dependencies": { 1651 - "node-fetch": "^2.6.0" 1652 - } 1653 }, 1654 "node_modules/package-json-from-dist": { 1655 "version": "1.0.1", ··· 2077 "node": ">=14.0.0" 2078 } 2079 }, 2080 - "node_modules/tr46": { 2081 - "version": "0.0.3", 2082 - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 2083 - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", 2084 - "license": "MIT" 2085 - }, 2086 "node_modules/typescript": { 2087 "version": "5.9.2", 2088 "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", ··· 2273 "jsdom": { 2274 "optional": true 2275 } 2276 - } 2277 - }, 2278 - "node_modules/webidl-conversions": { 2279 - "version": "3.0.1", 2280 - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 2281 - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", 2282 - "license": "BSD-2-Clause" 2283 - }, 2284 - "node_modules/whatwg-url": { 2285 - "version": "5.0.0", 2286 - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 2287 - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 2288 - "license": "MIT", 2289 - "dependencies": { 2290 - "tr46": "~0.0.3", 2291 - "webidl-conversions": "^3.0.0" 2292 } 2293 }, 2294 "node_modules/which": {
··· 10 "license": "MIT", 11 "dependencies": { 12 "json5": "2.2.3", 13 + "openweather-api-node": "3.1.5" 14 }, 15 "devDependencies": { 16 "@types/node": "24.3.0", ··· 1622 "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1623 } 1624 }, 1625 + "node_modules/openweather-api-node": { 1626 + "version": "3.1.5", 1627 + "resolved": "https://registry.npmjs.org/openweather-api-node/-/openweather-api-node-3.1.5.tgz", 1628 + "integrity": "sha512-FGLE0bWOTvp4XHaswmzMfisYMMEtwEwOEJR0vaS07L31OUcutV/UUO5/vRuktkRPoqfk3KZOoqddsRTGTxT7Aw==", 1629 + "license": "MIT" 1630 }, 1631 "node_modules/package-json-from-dist": { 1632 "version": "1.0.1", ··· 2054 "node": ">=14.0.0" 2055 } 2056 }, 2057 "node_modules/typescript": { 2058 "version": "5.9.2", 2059 "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", ··· 2244 "jsdom": { 2245 "optional": true 2246 } 2247 } 2248 }, 2249 "node_modules/which": {
+1 -1
package.json
··· 26 }, 27 "dependencies": { 28 "json5": "2.2.3", 29 - "openweathermap-ts": "1.2.10" 30 }, 31 "devDependencies": { 32 "typescript": "5.9.2",
··· 26 }, 27 "dependencies": { 28 "json5": "2.2.3", 29 + "openweather-api-node": "3.1.5" 30 }, 31 "devDependencies": { 32 "typescript": "5.9.2",
+3 -3
src/index.ts
··· 4 import Sequencer from './sequencer.js'; 5 import type {Programs, Segments, Sequences} from './sequencer.js'; 6 import type { Voices } from './voice.js'; 7 - import type { WeatherConfig } from './weather.js'; 8 9 10 interface Config { ··· 12 segments: Segments, 13 sequences: Sequences, 14 voices: Voices, 15 - weather: WeatherConfig 16 } 17 18 console.log('morning-report\nCory Sanin 2025\n'); 19 20 const config: Config = json5.parse(await fsp.readFile(process.env['CONFIG'] || path.join('config', 'config.json5'), { encoding: 'utf-8' })); 21 - const sequence = Sequencer(config); 22 console.log(sequence.join('\n')); 23 24
··· 4 import Sequencer from './sequencer.js'; 5 import type {Programs, Segments, Sequences} from './sequencer.js'; 6 import type { Voices } from './voice.js'; 7 + import type { Options } from 'openweather-api-node'; 8 9 10 interface Config { ··· 12 segments: Segments, 13 sequences: Sequences, 14 voices: Voices, 15 + weather: Options 16 } 17 18 console.log('morning-report\nCory Sanin 2025\n'); 19 20 const config: Config = json5.parse(await fsp.readFile(process.env['CONFIG'] || path.join('config', 'config.json5'), { encoding: 'utf-8' })); 21 + const sequence = await Sequencer(config); 22 console.log(sequence.join('\n')); 23 24
+61 -13
src/sequencer.ts
··· 1 import type { Config } from './index.js'; 2 3 type SegmentName = string; 4 type SequenceName = string; 5 type Programs = SegmentName[][]; 6 type Segments = { [segment: SegmentName]: SequenceName[] }; 7 type Sequence = { 8 - condition?: string; 9 tracks: string[]; 10 } 11 type Sequences = { [sequence: SequenceName]: Sequence }; ··· 16 return arr[Math.floor(Math.random() * arr.length)]; 17 } 18 19 - function conditionIsMet(condition: string | undefined = undefined): boolean { 20 if (typeof condition !== 'string') { 21 return true; 22 } 23 - // TODO: parse condition, return bool 24 - return false; 25 } 26 27 - function processSequence(sequence: Sequence): string[] { 28 const tracks = sequence.tracks; 29 - // TODO: process voice macros 30 - return tracks; 31 } 32 33 - function processSegment(segment: SegmentName): string[] { 34 if (!(segment in config.segments)) { 35 - return processSequence(config.sequences[segment]); 36 } 37 - const potentialSequences: SequenceName[] = config.segments[segment].filter(s => conditionIsMet(config.sequences[s].condition)); 38 if (potentialSequences.length === 0) { 39 return []; 40 } 41 - return processSequence(config.sequences[selectOne(potentialSequences)]); 42 } 43 44 - function Sequencer(conf: Config): string[] { 45 config = conf; 46 const sequence: string[] = []; 47 const program: SegmentName[] = selectOne(conf.programs); 48 - program.forEach(segment => sequence.push(...processSegment(segment))); 49 return sequence; 50 } 51
··· 1 + import { OpenWeatherAPI, type CurrentWeather } from 'openweather-api-node'; 2 + import { voiceLines } from './voice.js'; 3 import type { Config } from './index.js'; 4 + import type { Voice } from './voice.js'; 5 6 type SegmentName = string; 7 type SequenceName = string; 8 type Programs = SegmentName[][]; 9 type Segments = { [segment: SegmentName]: SequenceName[] }; 10 type Sequence = { 11 + conditions?: string[]; 12 tracks: string[]; 13 } 14 type Sequences = { [sequence: SequenceName]: Sequence }; ··· 19 return arr[Math.floor(Math.random() * arr.length)]; 20 } 21 22 + function resolveSide(side: string, currentWeather: CurrentWeather) { 23 + if (!side.startsWith('weather')) { 24 + return side.includes('.') ? parseFloat(side) : parseInt(side); 25 + } 26 + 27 + const tokens = side.split('.'); 28 + let w = currentWeather; 29 + tokens.forEach(t => w = w[t]); 30 + return typeof w === 'object' ? JSON.stringify(w) : w as (string | number); 31 + } 32 + 33 + function conditionIsMet(condition: string | undefined, currentWeather: CurrentWeather): boolean { 34 if (typeof condition !== 'string') { 35 return true; 36 } 37 + const [lhs, relational, rhs] = condition.split(' '); 38 + const lhsResolved = resolveSide(lhs, currentWeather); 39 + const rhsResolved = resolveSide(rhs, currentWeather); 40 + switch (relational) { 41 + case '=': 42 + case '==': 43 + return lhsResolved == rhsResolved; 44 + case '<': 45 + return lhsResolved < rhsResolved; 46 + case '<=': 47 + return lhsResolved <= rhsResolved; 48 + case '>': 49 + return lhsResolved > rhsResolved; 50 + case '>=': 51 + return lhsResolved >= rhsResolved; 52 + default: 53 + throw new Error(`Unsupported relational operator: ${relational}`); 54 + } 55 } 56 57 + function resolveMacro(str: string, currentWeather: CurrentWeather): string[] { 58 + if (str.startsWith('%')) { 59 + const [profile, subject] = str.substring(1).split(' ', 2); 60 + const voiceProfile: Voice = config.voices[profile]; 61 + let resolvedSubject: any = currentWeather; 62 + subject.split('.').forEach(t => resolvedSubject = resolvedSubject[t]); 63 + return voiceLines(voiceProfile, resolvedSubject); 64 + } 65 + else if (str.startsWith('$')) { 66 + return null; 67 + } 68 + return [str]; 69 + } 70 + 71 + function processSequence(sequence: Sequence, currentWeather: CurrentWeather): string[] { 72 const tracks = sequence.tracks; 73 + return tracks.map(t => resolveMacro(t, currentWeather)).flat().filter(t => t !== null); 74 } 75 76 + function processSegment(segment: SegmentName, currentWeather: CurrentWeather): string[] { 77 if (!(segment in config.segments)) { 78 + return processSequence(config.sequences[segment], currentWeather); 79 } 80 + const potentialSequences: SequenceName[] = config.segments[segment].filter(s => (config.sequences[s].conditions || []).every(c => conditionIsMet(c, currentWeather))); 81 if (potentialSequences.length === 0) { 82 return []; 83 } 84 + return processSequence(config.sequences[selectOne(potentialSequences)], currentWeather); 85 } 86 87 + async function Sequencer(conf: Config): Promise<string[]> { 88 config = conf; 89 + const weather = new OpenWeatherAPI(conf.weather); 90 + const currentWeather = await weather.getCurrent(); 91 const sequence: string[] = []; 92 const program: SegmentName[] = selectOne(conf.programs); 93 + for (let i = 0; i < program.length; i++) { 94 + const segment = program[i]; 95 + sequence.push(...(processSegment(segment, currentWeather))); 96 + } 97 return sequence; 98 } 99
-101
src/weather.ts
··· 1 - import OpenWeatherMap from 'openweathermap-ts'; 2 - import type { ThreeHourResponse, CurrentResponse, CountryCode } from 'openweathermap-ts/dist/types/index.js'; 3 - 4 - interface CityName { 5 - cityName: string; 6 - state: string; 7 - countryCode: CountryCode; 8 - } 9 - 10 - interface WeatherConfig { 11 - key: string; 12 - lang?: string; 13 - coordinates?: number[] | string; 14 - zip?: number; 15 - country?: CountryCode; 16 - cityid?: number; 17 - city?: CityName; 18 - } 19 - 20 - type LocationType = 'coordinates' | 'zip' | 'cityid' | 'city' | null; 21 - 22 - function parseCoords(coords: number[] | string): number[] { 23 - if (typeof coords == 'string') { 24 - return coords.replace(/\s/g, '').split(',').map(Number.parseFloat); 25 - } 26 - return coords; 27 - } 28 - 29 - 30 - class Weather { 31 - private openWeather: OpenWeatherMap.default; 32 - private locationType: LocationType; 33 - private current: CurrentResponse; 34 - private threeDay: ThreeHourResponse; 35 - 36 - constructor(options: WeatherConfig) { 37 - this.locationType = this.current = this.threeDay = null; 38 - this.openWeather = new OpenWeatherMap.default({ 39 - apiKey: options.key 40 - }); 41 - if ('city' in options && 'cityName' in options.city && 'state' in options.city && 'countryCode' in options.city) { 42 - this.openWeather.setCityName(options.city); 43 - this.locationType = 'city'; 44 - } 45 - if ('cityid' in options) { 46 - this.openWeather.setCityId(options.cityid); 47 - this.locationType = 'cityid'; 48 - } 49 - if ('zip' in options && 'country' in options) { 50 - this.openWeather.setZipCode(options.zip, options.country) 51 - this.locationType = 'zip'; 52 - } 53 - if ('coordinates' in options) { 54 - const coords = parseCoords(options.coordinates); 55 - if (coords.length >= 2) { 56 - this.openWeather.setGeoCoordinates(coords[0], coords[1]); 57 - this.locationType = 'coordinates'; 58 - } 59 - } 60 - } 61 - 62 - async getCurrentWeather(): Promise<CurrentResponse> { 63 - if (this.current) { 64 - return this.current; 65 - } 66 - switch (this.locationType) { 67 - case 'city': 68 - return this.current = await this.openWeather.getCurrentWeatherByCityName(); 69 - case 'cityid': 70 - return this.current = await this.openWeather.getCurrentWeatherByCityId(); 71 - case 'zip': 72 - return this.current = await this.openWeather.getCurrentWeatherByZipcode(); 73 - case 'coordinates': 74 - return this.current = await this.openWeather.getCurrentWeatherByGeoCoordinates(); 75 - default: 76 - throw new Error(`Can't fetch weather for location type '${this.locationType}'`); 77 - } 78 - } 79 - 80 - async getThreeHourForecast(): Promise<ThreeHourResponse> { 81 - if (this.threeDay) { 82 - return this.threeDay; 83 - } 84 - switch (this.locationType) { 85 - case 'city': 86 - return this.threeDay = await this.openWeather.getThreeHourForecastByCityName(); 87 - case 'cityid': 88 - return this.threeDay = await this.openWeather.getThreeHourForecastByCityId(); 89 - case 'zip': 90 - return this.threeDay = await this.openWeather.getThreeHourForecastByZipcode(); 91 - case 'coordinates': 92 - return this.threeDay = await this.openWeather.getThreeHourForecastByGeoCoordinates(); 93 - default: 94 - throw new Error(`Can't fetch weather for location type '${this.locationType}'`); 95 - } 96 - } 97 - } 98 - 99 - export default Weather; 100 - export { Weather }; 101 - export type { WeatherConfig, CityName, CurrentResponse, ThreeHourResponse };
···
+43
test/sequencer.test.ts
···
··· 1 + import { describe, expect, it, vi, beforeEach } from 'vitest'; 2 + import { type CurrentWeather, type Options } from 'openweather-api-node'; 3 + import { Sequencer } from '../src/sequencer.js'; 4 + import type { Config } from '../src/index.js'; 5 + 6 + const dummyWeather: Options = { key: 'dummy' }; 7 + 8 + vi.mock('openweather-api-node', () => { 9 + return { 10 + OpenWeatherAPI: vi.fn().mockImplementation((_) => { 11 + return { 12 + getCurrent: vi.fn(() => {}) 13 + } 14 + }) 15 + } 16 + }); 17 + 18 + describe('sequencer', () => { 19 + it('can generate a list from a static config', async () => { 20 + expect(await Sequencer({ 21 + programs: [['sequence 1', 'segment 1', 'segment 2']], 22 + segments: { 23 + 'segment 1': ['sequence 1'], 24 + 'segment 2': ['sequence 2'] 25 + }, 26 + sequences: { 27 + 'sequence 1': { 28 + 'tracks': [ 29 + 'seq1.flac' 30 + ] 31 + }, 32 + 'sequence 2': { 33 + 'tracks': [ 34 + 'seq2.flac' 35 + ] 36 + } 37 + }, 38 + voices: {}, 39 + weather: dummyWeather 40 + })).to.include.ordered.members(['seq1.flac', 'seq1.flac', 'seq2.flac']); 41 + }); 42 + }); 43 +
-332
test/weather.test.ts
··· 1 - import OpenWeatherMap from 'openweathermap-ts'; 2 - import { describe, expect, it, vi, beforeEach, type Mocked } from 'vitest'; 3 - import { Weather } from '../src/weather.js'; 4 - import type { CurrentResponse, ThreeHourResponse } from '../src/weather.js'; 5 - 6 - // #region mock API responses 7 - const current: CurrentResponse = { 8 - "coord": { 9 - "lon": 7.367, 10 - "lat": 45.133 11 - }, 12 - "weather": [ 13 - { 14 - "id": 501, 15 - "main": "Rain", 16 - "description": "moderate rain", 17 - "icon": "10d" 18 - } 19 - ], 20 - "base": "stations", 21 - "main": { 22 - "temp": 284.2, 23 - "feels_like": 282.93, 24 - "temp_min": 283.06, 25 - "temp_max": 286.82, 26 - "pressure": 1021, 27 - "humidity": 60 28 - }, 29 - "visibility": 10000, 30 - "wind": { 31 - "speed": 4.09, 32 - "deg": 121 33 - }, 34 - "clouds": { 35 - "all": 83 36 - }, 37 - "dt": 1726660758, 38 - "sys": { 39 - "type": 1, 40 - "id": 6736, 41 - "country": "IT", 42 - "sunrise": 1726636384, 43 - "sunset": 1726680975 44 - }, 45 - "timezone": 7200, 46 - "id": 3165523, 47 - "name": "Province of Turin", 48 - "cod": 200 49 - }; 50 - 51 - const threeHour: ThreeHourResponse = { 52 - "cod": "200", 53 - "message": 0, 54 - "cnt": 96, 55 - "list": [ 56 - { 57 - "dt": 1661875200, 58 - "main": { 59 - "temp": 296.34, 60 - "temp_min": 296.34, 61 - "temp_max": 298.24, 62 - "pressure": 1015, 63 - "sea_level": 1015, 64 - "grnd_level": 933, 65 - "humidity": 50, 66 - "temp_kf": -1.9 67 - }, 68 - "weather": [ 69 - { 70 - "id": 500, 71 - "main": "Rain", 72 - "description": "light rain", 73 - "icon": "10d" 74 - } 75 - ], 76 - "clouds": { 77 - "all": 97 78 - }, 79 - "wind": { 80 - "speed": 1.06, 81 - "deg": 66 82 - }, 83 - "rain": { 84 - "3h": 1 85 - }, 86 - "sys": { 87 - "pod": "d" 88 - }, 89 - "dt_txt": "2022-08-30 16:00:00" 90 - }, 91 - { 92 - "dt": 1661878800, 93 - "main": { 94 - "temp": 296.31, 95 - "temp_min": 296.2, 96 - "temp_max": 296.31, 97 - "pressure": 1015, 98 - "sea_level": 1015, 99 - "grnd_level": 932, 100 - "humidity": 53, 101 - "temp_kf": 0.11 102 - }, 103 - "weather": [ 104 - { 105 - "id": 500, 106 - "main": "Rain", 107 - "description": "light rain", 108 - "icon": "10d" 109 - } 110 - ], 111 - "clouds": { 112 - "all": 95 113 - }, 114 - "wind": { 115 - "speed": 1.58, 116 - "deg": 103 117 - }, 118 - "rain": { 119 - "3h": 0.24 120 - }, 121 - "sys": { 122 - "pod": "d" 123 - }, 124 - "dt_txt": "2022-08-30 17:00:00" 125 - }, 126 - { 127 - "dt": 1661882400, 128 - "main": { 129 - "temp": 294.94, 130 - "temp_min": 292.84, 131 - "temp_max": 294.94, 132 - "pressure": 1015, 133 - "sea_level": 1015, 134 - "grnd_level": 931, 135 - "humidity": 60, 136 - "temp_kf": 2.1 137 - }, 138 - "weather": [ 139 - { 140 - "id": 500, 141 - "main": "Rain", 142 - "description": "light rain", 143 - "icon": "10n" 144 - } 145 - ], 146 - "clouds": { 147 - "all": 93 148 - }, 149 - "wind": { 150 - "speed": 1.97, 151 - "deg": 157 152 - }, 153 - "rain": { 154 - "3h": 0.2 155 - }, 156 - "sys": { 157 - "pod": "n" 158 - }, 159 - "dt_txt": "2022-08-30 18:00:00" 160 - }, 161 - { 162 - "dt": 1662217200, 163 - "main": { 164 - "temp": 294.14, 165 - "temp_min": 294.14, 166 - "temp_max": 294.14, 167 - "pressure": 1014, 168 - "sea_level": 1014, 169 - "grnd_level": 931, 170 - "humidity": 65, 171 - "temp_kf": 0 172 - }, 173 - "weather": [ 174 - { 175 - "id": 804, 176 - "main": "Clouds", 177 - "description": "overcast clouds", 178 - "icon": "04d" 179 - } 180 - ], 181 - "clouds": { 182 - "all": 100 183 - }, 184 - "wind": { 185 - "speed": 0.91, 186 - "deg": 104 187 - }, 188 - "sys": { 189 - "pod": "d" 190 - }, 191 - "dt_txt": "2022-09-03 15:00:00" 192 - } 193 - ], 194 - "city": { 195 - "id": 3163858, 196 - "name": "Zocca", 197 - "coord": { 198 - "lat": 44.34, 199 - "lon": 10.99 200 - }, 201 - "country": "IT" 202 - } 203 - } 204 - // #endregion 205 - 206 - vi.mock('openweathermap-ts', () => { 207 - return { 208 - default: { 209 - default: vi.fn().mockImplementation((_) => { 210 - return { 211 - setCityName: vi.fn(() => undefined), 212 - setCityId: vi.fn(() => undefined), 213 - setZipCode: vi.fn(() => undefined), 214 - setGeoCoordinates: vi.fn(() => undefined), 215 - getCurrentWeatherByCityName: vi.fn(async () => current), 216 - getCurrentWeatherByCityId: vi.fn(async () => current), 217 - getCurrentWeatherByZipcode: vi.fn(async () => current), 218 - getCurrentWeatherByGeoCoordinates: vi.fn(async () => current), 219 - getThreeHourForecastByCityName: vi.fn(async () => threeHour), 220 - getThreeHourForecastByCityId: vi.fn(async () => threeHour), 221 - getThreeHourForecastByZipcode: vi.fn(async () => threeHour), 222 - getThreeHourForecastByGeoCoordinates: vi.fn(async () => threeHour), 223 - } 224 - }) 225 - } 226 - } 227 - }); 228 - 229 - let weather: Weather = null; 230 - 231 - describe.for( 232 - [ 233 - { 234 - weatherFactory: () => new Weather({ 235 - key: 'api-key', 236 - city: { 237 - cityName: 'Madison', 238 - state: 'Wisconsin', 239 - countryCode: 'US' 240 - } 241 - }), 242 - by: 'CityName' 243 - }, 244 - { 245 - weatherFactory: () => new Weather({ 246 - key: 'api-key', 247 - cityid: 1 248 - }), 249 - by: 'CityId' 250 - }, 251 - { 252 - weatherFactory: () => new Weather({ 253 - key: 'api-key', 254 - zip: 53702, 255 - country: 'US' 256 - }), 257 - by: 'Zipcode' 258 - }, 259 - { 260 - weatherFactory: () => new Weather({ 261 - key: 'api-key', 262 - coordinates: [0, 0] 263 - }), 264 - by: 'GeoCoordinates' 265 - }, 266 - { 267 - weatherFactory: () => new Weather({ 268 - key: 'api-key', 269 - coordinates: '1000 , 1000' 270 - }), 271 - by: 'GeoCoordinates' 272 - } 273 - ])('weather API using city name', ({ weatherFactory, by }) => { 274 - beforeEach(() => { 275 - vi.clearAllMocks(); 276 - weather = weatherFactory(); 277 - }); 278 - 279 - it('gets current weather', async () => { 280 - expect(await weather.getCurrentWeather()).to.deep.equal(current); 281 - expect(OpenWeatherMap.default).toBeCalledWith({ 282 - apiKey: 'api-key' 283 - }); 284 - const MockedOpenWeather = vi.mocked(OpenWeatherMap.default); 285 - const openWeather = MockedOpenWeather.mock.results[0].value; 286 - expect(openWeather[`getCurrentWeatherBy${by}`]).toBeCalledTimes(1); 287 - }); 288 - 289 - it('gets the 3h forecast', async () => { 290 - expect(await weather.getThreeHourForecast()).to.deep.equal(threeHour); 291 - expect(OpenWeatherMap.default).toBeCalledWith({ 292 - apiKey: 'api-key' 293 - }); 294 - const MockedOpenWeather = vi.mocked(OpenWeatherMap.default); 295 - const openWeather = MockedOpenWeather.mock.results[0].value; 296 - expect(openWeather[`getThreeHourForecastBy${by}`]).toBeCalledTimes(1); 297 - }); 298 - 299 - it('only calls the api once', async () => { 300 - expect(await weather.getCurrentWeather()).to.deep.equal(current); 301 - expect(await weather.getCurrentWeather()).to.deep.equal(current); 302 - expect(await weather.getThreeHourForecast()).to.deep.equal(threeHour); 303 - expect(await weather.getThreeHourForecast()).to.deep.equal(threeHour); 304 - expect(OpenWeatherMap.default).toBeCalledWith({ 305 - apiKey: 'api-key' 306 - }); 307 - const MockedOpenWeather = vi.mocked(OpenWeatherMap.default); 308 - const openWeather = MockedOpenWeather.mock.results[0].value; 309 - expect(openWeather[`getCurrentWeatherBy${by}`]).toBeCalledTimes(1); 310 - expect(openWeather[`getThreeHourForecastBy${by}`]).toBeCalledTimes(1); 311 - }); 312 - } 313 - ); 314 - 315 - describe('invalid weather object', () => { 316 - beforeEach(() => { 317 - vi.clearAllMocks(); 318 - weather = new Weather({ 319 - key: 'api-key', 320 - cityid: 1 321 - }); 322 - weather['locationType'] = null; 323 - }); 324 - 325 - it('throws an exception when getCurrentWeather is called', async () => { 326 - await expect(weather.getCurrentWeather()).rejects.toThrow(/location type/); 327 - }); 328 - 329 - it('throws an exception when getThreeHourForecast is called', async () => { 330 - await expect(weather.getThreeHourForecast()).rejects.toThrow(/location type/); 331 - }); 332 - });
···