+6
-51
package-lock.json
+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
+1
-1
package.json
+3
-3
src/index.ts
+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
+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
-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
+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
-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
-
});
···