switch out weather library
This commit is contained in:
57
package-lock.json
generated
57
package-lock.json
generated
@@ -10,7 +10,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"openweathermap-ts": "1.2.10"
|
"openweather-api-node": "3.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "24.3.0",
|
"@types/node": "24.3.0",
|
||||||
@@ -1622,34 +1622,11 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-fetch": {
|
"node_modules/openweather-api-node": {
|
||||||
"version": "2.7.0",
|
"version": "3.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/openweather-api-node/-/openweather-api-node-3.1.5.tgz",
|
||||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
"integrity": "sha512-FGLE0bWOTvp4XHaswmzMfisYMMEtwEwOEJR0vaS07L31OUcutV/UUO5/vRuktkRPoqfk3KZOoqddsRTGTxT7Aw==",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"dependencies": {
|
|
||||||
"whatwg-url": "^5.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "4.x || >=6.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"encoding": "^0.1.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"encoding": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/openweathermap-ts": {
|
|
||||||
"version": "1.2.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/openweathermap-ts/-/openweathermap-ts-1.2.10.tgz",
|
|
||||||
"integrity": "sha512-Zckv2aXN8ENSeAeroces2jJciLWb6aLNXEmvG6pmF+BcIMw2kwRo6++/AKUNoU5suOp47UWA6lllDV0TNm//OA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"node-fetch": "^2.6.0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/package-json-from-dist": {
|
"node_modules/package-json-from-dist": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@@ -2077,12 +2054,6 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tr46": {
|
|
||||||
"version": "0.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
|
||||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.2",
|
"version": "5.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||||
@@ -2275,22 +2246,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webidl-conversions": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
|
||||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
|
||||||
"license": "BSD-2-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/whatwg-url": {
|
|
||||||
"version": "5.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
|
||||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"tr46": "~0.0.3",
|
|
||||||
"webidl-conversions": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
@@ -26,7 +26,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"openweathermap-ts": "1.2.10"
|
"openweather-api-node": "3.1.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "5.9.2",
|
"typescript": "5.9.2",
|
||||||
|
@@ -4,7 +4,7 @@ import json5 from 'json5';
|
|||||||
import Sequencer from './sequencer.js';
|
import Sequencer from './sequencer.js';
|
||||||
import type {Programs, Segments, Sequences} from './sequencer.js';
|
import type {Programs, Segments, Sequences} from './sequencer.js';
|
||||||
import type { Voices } from './voice.js';
|
import type { Voices } from './voice.js';
|
||||||
import type { WeatherConfig } from './weather.js';
|
import type { Options } from 'openweather-api-node';
|
||||||
|
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
@@ -12,13 +12,13 @@ interface Config {
|
|||||||
segments: Segments,
|
segments: Segments,
|
||||||
sequences: Sequences,
|
sequences: Sequences,
|
||||||
voices: Voices,
|
voices: Voices,
|
||||||
weather: WeatherConfig
|
weather: Options
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('morning-report\nCory Sanin 2025\n');
|
console.log('morning-report\nCory Sanin 2025\n');
|
||||||
|
|
||||||
const config: Config = json5.parse(await fsp.readFile(process.env['CONFIG'] || path.join('config', 'config.json5'), { encoding: 'utf-8' }));
|
const config: Config = json5.parse(await fsp.readFile(process.env['CONFIG'] || path.join('config', 'config.json5'), { encoding: 'utf-8' }));
|
||||||
const sequence = Sequencer(config);
|
const sequence = await Sequencer(config);
|
||||||
console.log(sequence.join('\n'));
|
console.log(sequence.join('\n'));
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,11 +1,14 @@
|
|||||||
|
import { OpenWeatherAPI, type CurrentWeather } from 'openweather-api-node';
|
||||||
|
import { voiceLines } from './voice.js';
|
||||||
import type { Config } from './index.js';
|
import type { Config } from './index.js';
|
||||||
|
import type { Voice } from './voice.js';
|
||||||
|
|
||||||
type SegmentName = string;
|
type SegmentName = string;
|
||||||
type SequenceName = string;
|
type SequenceName = string;
|
||||||
type Programs = SegmentName[][];
|
type Programs = SegmentName[][];
|
||||||
type Segments = { [segment: SegmentName]: SequenceName[] };
|
type Segments = { [segment: SegmentName]: SequenceName[] };
|
||||||
type Sequence = {
|
type Sequence = {
|
||||||
condition?: string;
|
conditions?: string[];
|
||||||
tracks: string[];
|
tracks: string[];
|
||||||
}
|
}
|
||||||
type Sequences = { [sequence: SequenceName]: Sequence };
|
type Sequences = { [sequence: SequenceName]: Sequence };
|
||||||
@@ -16,36 +19,81 @@ function selectOne<T>(arr: T[]): T {
|
|||||||
return arr[Math.floor(Math.random() * arr.length)];
|
return arr[Math.floor(Math.random() * arr.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
function conditionIsMet(condition: string | undefined = undefined): boolean {
|
function resolveSide(side: string, currentWeather: CurrentWeather) {
|
||||||
|
if (!side.startsWith('weather')) {
|
||||||
|
return side.includes('.') ? parseFloat(side) : parseInt(side);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = side.split('.');
|
||||||
|
let w = currentWeather;
|
||||||
|
tokens.forEach(t => w = w[t]);
|
||||||
|
return typeof w === 'object' ? JSON.stringify(w) : w as (string | number);
|
||||||
|
}
|
||||||
|
|
||||||
|
function conditionIsMet(condition: string | undefined, currentWeather: CurrentWeather): boolean {
|
||||||
if (typeof condition !== 'string') {
|
if (typeof condition !== 'string') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// TODO: parse condition, return bool
|
const [lhs, relational, rhs] = condition.split(' ');
|
||||||
return false;
|
const lhsResolved = resolveSide(lhs, currentWeather);
|
||||||
|
const rhsResolved = resolveSide(rhs, currentWeather);
|
||||||
|
switch (relational) {
|
||||||
|
case '=':
|
||||||
|
case '==':
|
||||||
|
return lhsResolved == rhsResolved;
|
||||||
|
case '<':
|
||||||
|
return lhsResolved < rhsResolved;
|
||||||
|
case '<=':
|
||||||
|
return lhsResolved <= rhsResolved;
|
||||||
|
case '>':
|
||||||
|
return lhsResolved > rhsResolved;
|
||||||
|
case '>=':
|
||||||
|
return lhsResolved >= rhsResolved;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported relational operator: ${relational}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function processSequence(sequence: Sequence): string[] {
|
function resolveMacro(str: string, currentWeather: CurrentWeather): string[] {
|
||||||
|
if (str.startsWith('%')) {
|
||||||
|
const [profile, subject] = str.substring(1).split(' ', 2);
|
||||||
|
const voiceProfile: Voice = config.voices[profile];
|
||||||
|
let resolvedSubject: any = currentWeather;
|
||||||
|
subject.split('.').forEach(t => resolvedSubject = resolvedSubject[t]);
|
||||||
|
return voiceLines(voiceProfile, resolvedSubject);
|
||||||
|
}
|
||||||
|
else if (str.startsWith('$')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return [str];
|
||||||
|
}
|
||||||
|
|
||||||
|
function processSequence(sequence: Sequence, currentWeather: CurrentWeather): string[] {
|
||||||
const tracks = sequence.tracks;
|
const tracks = sequence.tracks;
|
||||||
// TODO: process voice macros
|
return tracks.map(t => resolveMacro(t, currentWeather)).flat().filter(t => t !== null);
|
||||||
return tracks;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function processSegment(segment: SegmentName): string[] {
|
function processSegment(segment: SegmentName, currentWeather: CurrentWeather): string[] {
|
||||||
if (!(segment in config.segments)) {
|
if (!(segment in config.segments)) {
|
||||||
return processSequence(config.sequences[segment]);
|
return processSequence(config.sequences[segment], currentWeather);
|
||||||
}
|
}
|
||||||
const potentialSequences: SequenceName[] = config.segments[segment].filter(s => conditionIsMet(config.sequences[s].condition));
|
const potentialSequences: SequenceName[] = config.segments[segment].filter(s => (config.sequences[s].conditions || []).every(c => conditionIsMet(c, currentWeather)));
|
||||||
if (potentialSequences.length === 0) {
|
if (potentialSequences.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return processSequence(config.sequences[selectOne(potentialSequences)]);
|
return processSequence(config.sequences[selectOne(potentialSequences)], currentWeather);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Sequencer(conf: Config): string[] {
|
async function Sequencer(conf: Config): Promise<string[]> {
|
||||||
config = conf;
|
config = conf;
|
||||||
|
const weather = new OpenWeatherAPI(conf.weather);
|
||||||
|
const currentWeather = await weather.getCurrent();
|
||||||
const sequence: string[] = [];
|
const sequence: string[] = [];
|
||||||
const program: SegmentName[] = selectOne(conf.programs);
|
const program: SegmentName[] = selectOne(conf.programs);
|
||||||
program.forEach(segment => sequence.push(...processSegment(segment)));
|
for (let i = 0; i < program.length; i++) {
|
||||||
|
const segment = program[i];
|
||||||
|
sequence.push(...(processSegment(segment, currentWeather)));
|
||||||
|
}
|
||||||
return sequence;
|
return sequence;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
101
src/weather.ts
101
src/weather.ts
@@ -1,101 +0,0 @@
|
|||||||
import OpenWeatherMap from 'openweathermap-ts';
|
|
||||||
import type { ThreeHourResponse, CurrentResponse, CountryCode } from 'openweathermap-ts/dist/types/index.js';
|
|
||||||
|
|
||||||
interface CityName {
|
|
||||||
cityName: string;
|
|
||||||
state: string;
|
|
||||||
countryCode: CountryCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WeatherConfig {
|
|
||||||
key: string;
|
|
||||||
lang?: string;
|
|
||||||
coordinates?: number[] | string;
|
|
||||||
zip?: number;
|
|
||||||
country?: CountryCode;
|
|
||||||
cityid?: number;
|
|
||||||
city?: CityName;
|
|
||||||
}
|
|
||||||
|
|
||||||
type LocationType = 'coordinates' | 'zip' | 'cityid' | 'city' | null;
|
|
||||||
|
|
||||||
function parseCoords(coords: number[] | string): number[] {
|
|
||||||
if (typeof coords == 'string') {
|
|
||||||
return coords.replace(/\s/g, '').split(',').map(Number.parseFloat);
|
|
||||||
}
|
|
||||||
return coords;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Weather {
|
|
||||||
private openWeather: OpenWeatherMap.default;
|
|
||||||
private locationType: LocationType;
|
|
||||||
private current: CurrentResponse;
|
|
||||||
private threeDay: ThreeHourResponse;
|
|
||||||
|
|
||||||
constructor(options: WeatherConfig) {
|
|
||||||
this.locationType = this.current = this.threeDay = null;
|
|
||||||
this.openWeather = new OpenWeatherMap.default({
|
|
||||||
apiKey: options.key
|
|
||||||
});
|
|
||||||
if ('city' in options && 'cityName' in options.city && 'state' in options.city && 'countryCode' in options.city) {
|
|
||||||
this.openWeather.setCityName(options.city);
|
|
||||||
this.locationType = 'city';
|
|
||||||
}
|
|
||||||
if ('cityid' in options) {
|
|
||||||
this.openWeather.setCityId(options.cityid);
|
|
||||||
this.locationType = 'cityid';
|
|
||||||
}
|
|
||||||
if ('zip' in options && 'country' in options) {
|
|
||||||
this.openWeather.setZipCode(options.zip, options.country)
|
|
||||||
this.locationType = 'zip';
|
|
||||||
}
|
|
||||||
if ('coordinates' in options) {
|
|
||||||
const coords = parseCoords(options.coordinates);
|
|
||||||
if (coords.length >= 2) {
|
|
||||||
this.openWeather.setGeoCoordinates(coords[0], coords[1]);
|
|
||||||
this.locationType = 'coordinates';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCurrentWeather(): Promise<CurrentResponse> {
|
|
||||||
if (this.current) {
|
|
||||||
return this.current;
|
|
||||||
}
|
|
||||||
switch (this.locationType) {
|
|
||||||
case 'city':
|
|
||||||
return this.current = await this.openWeather.getCurrentWeatherByCityName();
|
|
||||||
case 'cityid':
|
|
||||||
return this.current = await this.openWeather.getCurrentWeatherByCityId();
|
|
||||||
case 'zip':
|
|
||||||
return this.current = await this.openWeather.getCurrentWeatherByZipcode();
|
|
||||||
case 'coordinates':
|
|
||||||
return this.current = await this.openWeather.getCurrentWeatherByGeoCoordinates();
|
|
||||||
default:
|
|
||||||
throw new Error(`Can't fetch weather for location type '${this.locationType}'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getThreeHourForecast(): Promise<ThreeHourResponse> {
|
|
||||||
if (this.threeDay) {
|
|
||||||
return this.threeDay;
|
|
||||||
}
|
|
||||||
switch (this.locationType) {
|
|
||||||
case 'city':
|
|
||||||
return this.threeDay = await this.openWeather.getThreeHourForecastByCityName();
|
|
||||||
case 'cityid':
|
|
||||||
return this.threeDay = await this.openWeather.getThreeHourForecastByCityId();
|
|
||||||
case 'zip':
|
|
||||||
return this.threeDay = await this.openWeather.getThreeHourForecastByZipcode();
|
|
||||||
case 'coordinates':
|
|
||||||
return this.threeDay = await this.openWeather.getThreeHourForecastByGeoCoordinates();
|
|
||||||
default:
|
|
||||||
throw new Error(`Can't fetch weather for location type '${this.locationType}'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Weather;
|
|
||||||
export { Weather };
|
|
||||||
export type { WeatherConfig, CityName, CurrentResponse, ThreeHourResponse };
|
|
43
test/sequencer.test.ts
Normal file
43
test/sequencer.test.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||||
|
import { type CurrentWeather, type Options } from 'openweather-api-node';
|
||||||
|
import { Sequencer } from '../src/sequencer.js';
|
||||||
|
import type { Config } from '../src/index.js';
|
||||||
|
|
||||||
|
const dummyWeather: Options = { key: 'dummy' };
|
||||||
|
|
||||||
|
vi.mock('openweather-api-node', () => {
|
||||||
|
return {
|
||||||
|
OpenWeatherAPI: vi.fn().mockImplementation((_) => {
|
||||||
|
return {
|
||||||
|
getCurrent: vi.fn(() => {})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sequencer', () => {
|
||||||
|
it('can generate a list from a static config', async () => {
|
||||||
|
expect(await Sequencer({
|
||||||
|
programs: [['sequence 1', 'segment 1', 'segment 2']],
|
||||||
|
segments: {
|
||||||
|
'segment 1': ['sequence 1'],
|
||||||
|
'segment 2': ['sequence 2']
|
||||||
|
},
|
||||||
|
sequences: {
|
||||||
|
'sequence 1': {
|
||||||
|
'tracks': [
|
||||||
|
'seq1.flac'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'sequence 2': {
|
||||||
|
'tracks': [
|
||||||
|
'seq2.flac'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
voices: {},
|
||||||
|
weather: dummyWeather
|
||||||
|
})).to.include.ordered.members(['seq1.flac', 'seq1.flac', 'seq2.flac']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@@ -1,332 +0,0 @@
|
|||||||
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
|
|
||||||
},
|
|
||||||
"visibility": 10000,
|
|
||||||
"wind": {
|
|
||||||
"speed": 4.09,
|
|
||||||
"deg": 121
|
|
||||||
},
|
|
||||||
"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/);
|
|
||||||
});
|
|
||||||
});
|
|
Reference in New Issue
Block a user