Procedurally generates a radio weather report
1import { describe, expect, it, vi } from 'vitest';
2import { type Options } from 'openweather-api-node';
3import { Sequencer } from '../src/sequencer.js';
4
5const dummyWeather: Options = { key: 'dummy' };
6
7vi.mock('openweather-api-node', () => {
8 return {
9 OpenWeatherAPI: vi.fn().mockImplementation((_) => {
10 return {
11 getToday: vi.fn(() => {
12 return {
13 "lat": 43.0748,
14 "lon": -89.3838,
15 "dt": "2025-08-29T06:31:05.000Z",
16 "dtRaw": 1756449065,
17 "timezoneOffset": -18000,
18 "astronomical": {
19 "sunrise": "2025-08-29T11:19:05.000Z",
20 "sunriseRaw": 1756466345,
21 "sunset": "2025-08-30T00:38:08.000Z",
22 "sunsetRaw": 1756514288
23 },
24 "weather": {
25 "temp": {
26 "cur": 55.85,
27 "min": 52.99,
28 "max": 58.01
29 },
30 "feelsLike": {
31 "cur": 55.31
32 },
33 "pressure": 1022,
34 "humidity": 89,
35 "clouds": 0,
36 "visibility": 10000,
37 "wind": {
38 "deg": 140,
39 "speed": 5.75
40 },
41 "rain": 0,
42 "snow": 0,
43 "conditionId": 800,
44 "main": "Clear",
45 "description": "clear sky",
46 "icon": {
47 "url": "http://openweathermap.org/img/wn/01n@2x.png",
48 "raw": "01n"
49 }
50 }
51 }
52 })
53 }
54 })
55 }
56});
57
58describe('sequencer', () => {
59 it('can generate a list from a static config', async () => {
60 expect(await Sequencer({
61 programs: [['sequence 1', 'segment 1', 'segment 2']],
62 segments: {
63 'segment 1': ['sequence 1'],
64 'segment 2': ['sequence 2']
65 },
66 sequences: {
67 'sequence 1': {
68 'tracks': [
69 'seq1.flac'
70 ]
71 },
72 'sequence 2': {
73 'tracks': [
74 'seq2.flac'
75 ]
76 }
77 },
78 voices: {},
79 weather: dummyWeather
80 })).to.include.ordered.members(['seq1.flac', 'seq1.flac', 'seq2.flac']);
81 });
82
83 it('can include tracks conditionally', async () => {
84 expect(await Sequencer({
85 programs: [['segment 1', 'sequence 1', 'segment 2', 'sequence 2']],
86 segments: {
87 'segment 1': ['sequence 1'],
88 'segment 2': ['sequence 2']
89 },
90 sequences: {
91 'sequence 1': {
92 'conditions': ['1 = 1', '1.1 > 1', '2 >= 1', '1 < 2', '1 <= 1', '-1 != 0', undefined],
93 'tracks': [
94 'seq1.flac'
95 ]
96 },
97 'sequence 2': {
98 'conditions': ['weather.lat = -500'],
99 'tracks': [
100 'seq2.flac'
101 ]
102 }
103 },
104 voices: {},
105 weather: dummyWeather
106 })).to.be.ordered.members(['seq1.flac', 'seq1.flac']);
107 });
108
109 it('throws an error on invalid conditions', async () => {
110 await expect(Sequencer({
111 programs: [['sequence 1']],
112 segments: {
113 },
114 sequences: {
115 'sequence 1': {
116 'conditions': ['100'],
117 'tracks': [
118 'seq1.flac'
119 ]
120 }
121 },
122 voices: {},
123 weather: dummyWeather
124 })).rejects.toThrow(/not in the correct format/);
125
126 await expect(Sequencer({
127 programs: [['sequence 1']],
128 segments: {
129 },
130 sequences: {
131 'sequence 1': {
132 'conditions': ['1 ~ 2'],
133 'tracks': [
134 'seq1.flac'
135 ]
136 }
137 },
138 voices: {},
139 weather: dummyWeather
140 })).rejects.toThrow(/Unsupported relational operator/);
141 });
142
143 it('can stringify conditions', async () => {
144 expect(await Sequencer({
145 programs: [['sequence 1']],
146 segments: {
147 },
148 sequences: {
149 'sequence 1': {
150 'conditions': ['weather.feelsLike = {"cur":55.31}'],
151 'tracks': [
152 'seq1.flac'
153 ]
154 }
155 },
156 voices: {},
157 weather: dummyWeather
158 })).to.be.ordered.members(['seq1.flac']);
159 });
160
161 it('can parse voice macros', async () => {
162 expect(await Sequencer({
163 programs: [['sequence 1']],
164 segments: {
165 },
166 sequences: {
167 'sequence 1': {
168 'tracks': [
169 '%alice weather.temp.max'
170 ]
171 }
172 },
173 voices: {
174 "alice": {
175 "directory": "alice/",
176 "extension": "flac"
177 }
178 },
179 weather: dummyWeather
180 })).to.be.ordered.members([
181 'alice/fifty.flac',
182 'alice/eight.flac',
183 'alice/point.flac',
184 'alice/zero.flac',
185 'alice/one.flac'
186 ]);
187 });
188});
189