@xiboplayer/settings 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +293 -0
- package/package.json +37 -0
- package/src/index.js +7 -0
- package/src/settings.js +352 -0
- package/src/settings.test.js +480 -0
- package/vitest.config.js +9 -0
- package/vitest.setup.js +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# @xiboplayer/settings
|
|
2
|
+
|
|
3
|
+
CMS display settings management and application for Xibo players.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Parse all settings from RegisterDisplay CMS response
|
|
8
|
+
- Validate and normalize setting values
|
|
9
|
+
- Dynamic collection interval updates
|
|
10
|
+
- Download window enforcement (same-day and overnight windows)
|
|
11
|
+
- Screenshot interval tracking
|
|
12
|
+
- Event emission on setting changes
|
|
13
|
+
- Comprehensive error handling
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @xiboplayer/settings
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Basic Usage
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
import { DisplaySettings } from '@xiboplayer/settings';
|
|
27
|
+
|
|
28
|
+
const displaySettings = new DisplaySettings();
|
|
29
|
+
|
|
30
|
+
// Apply settings from RegisterDisplay response
|
|
31
|
+
const result = displaySettings.applySettings(cmsSettings);
|
|
32
|
+
|
|
33
|
+
console.log('Changed settings:', result.changed);
|
|
34
|
+
console.log('Collection interval:', result.settings.collectInterval);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### With PlayerCore
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
import { PlayerCore } from '@xiboplayer/core';
|
|
41
|
+
import { DisplaySettings } from '@xiboplayer/settings';
|
|
42
|
+
|
|
43
|
+
const displaySettings = new DisplaySettings();
|
|
44
|
+
|
|
45
|
+
const core = new PlayerCore({
|
|
46
|
+
config,
|
|
47
|
+
xmds,
|
|
48
|
+
cache,
|
|
49
|
+
schedule,
|
|
50
|
+
renderer,
|
|
51
|
+
xmrWrapper,
|
|
52
|
+
displaySettings // Inject DisplaySettings
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Settings are automatically applied after RegisterDisplay
|
|
56
|
+
await core.collect();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Event Handling
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
// Listen for collection interval changes
|
|
63
|
+
displaySettings.on('interval-changed', (newInterval) => {
|
|
64
|
+
console.log(`Collection interval changed to ${newInterval}s`);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Listen for any settings changes
|
|
68
|
+
displaySettings.on('settings-applied', (settings, changes) => {
|
|
69
|
+
console.log('Settings updated:', changes.join(', '));
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## API Reference
|
|
74
|
+
|
|
75
|
+
### Constructor
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
const displaySettings = new DisplaySettings();
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Methods
|
|
82
|
+
|
|
83
|
+
#### `applySettings(settings)`
|
|
84
|
+
|
|
85
|
+
Apply CMS settings from RegisterDisplay response.
|
|
86
|
+
|
|
87
|
+
**Parameters:**
|
|
88
|
+
- `settings` (Object): Raw settings from CMS
|
|
89
|
+
|
|
90
|
+
**Returns:**
|
|
91
|
+
- Object with `changed` (array) and `settings` (object)
|
|
92
|
+
|
|
93
|
+
**Example:**
|
|
94
|
+
```javascript
|
|
95
|
+
const result = displaySettings.applySettings({
|
|
96
|
+
collectInterval: 600,
|
|
97
|
+
displayName: 'Main Display',
|
|
98
|
+
statsEnabled: '1',
|
|
99
|
+
xmrWebSocketAddress: 'ws://xmr.example.com:9505'
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
console.log(result.changed); // ['collectInterval']
|
|
103
|
+
console.log(result.settings.collectInterval); // 600
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
#### `getCollectInterval()`
|
|
107
|
+
|
|
108
|
+
Get current collection interval in seconds.
|
|
109
|
+
|
|
110
|
+
**Returns:** Number (seconds)
|
|
111
|
+
|
|
112
|
+
#### `getDisplayName()`
|
|
113
|
+
|
|
114
|
+
Get display name from CMS.
|
|
115
|
+
|
|
116
|
+
**Returns:** String
|
|
117
|
+
|
|
118
|
+
#### `getDisplaySize()`
|
|
119
|
+
|
|
120
|
+
Get display dimensions.
|
|
121
|
+
|
|
122
|
+
**Returns:** Object `{ width: number, height: number }`
|
|
123
|
+
|
|
124
|
+
#### `isStatsEnabled()`
|
|
125
|
+
|
|
126
|
+
Check if stats/proof of play is enabled.
|
|
127
|
+
|
|
128
|
+
**Returns:** Boolean
|
|
129
|
+
|
|
130
|
+
#### `getAllSettings()`
|
|
131
|
+
|
|
132
|
+
Get all current settings as an object.
|
|
133
|
+
|
|
134
|
+
**Returns:** Object
|
|
135
|
+
|
|
136
|
+
#### `getSetting(key, defaultValue)`
|
|
137
|
+
|
|
138
|
+
Get a specific setting by key.
|
|
139
|
+
|
|
140
|
+
**Parameters:**
|
|
141
|
+
- `key` (String): Setting key
|
|
142
|
+
- `defaultValue` (Any): Default if not set
|
|
143
|
+
|
|
144
|
+
**Returns:** Setting value or default
|
|
145
|
+
|
|
146
|
+
#### `isInDownloadWindow()`
|
|
147
|
+
|
|
148
|
+
Check if current time is within the download window.
|
|
149
|
+
|
|
150
|
+
**Returns:** Boolean
|
|
151
|
+
|
|
152
|
+
**Example:**
|
|
153
|
+
```javascript
|
|
154
|
+
// Configure download window 09:00-17:00
|
|
155
|
+
displaySettings.applySettings({
|
|
156
|
+
downloadStartWindow: '09:00',
|
|
157
|
+
downloadEndWindow: '17:00'
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Check if downloads are allowed now
|
|
161
|
+
if (displaySettings.isInDownloadWindow()) {
|
|
162
|
+
await downloadFiles();
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### `getNextDownloadWindow()`
|
|
167
|
+
|
|
168
|
+
Get the next download window start time.
|
|
169
|
+
|
|
170
|
+
**Returns:** Date object or null
|
|
171
|
+
|
|
172
|
+
#### `shouldTakeScreenshot(lastScreenshot)`
|
|
173
|
+
|
|
174
|
+
Check if screenshot interval has elapsed.
|
|
175
|
+
|
|
176
|
+
**Parameters:**
|
|
177
|
+
- `lastScreenshot` (Date): Last screenshot timestamp
|
|
178
|
+
|
|
179
|
+
**Returns:** Boolean
|
|
180
|
+
|
|
181
|
+
## Supported Settings
|
|
182
|
+
|
|
183
|
+
### Collection
|
|
184
|
+
- `collectInterval` - Collection interval in seconds (60-86400, default: 300)
|
|
185
|
+
|
|
186
|
+
### Display Info
|
|
187
|
+
- `displayName` - Display name (default: 'Unknown Display')
|
|
188
|
+
- `sizeX` - Display width in pixels (default: 1920)
|
|
189
|
+
- `sizeY` - Display height in pixels (default: 1080)
|
|
190
|
+
|
|
191
|
+
### Stats/Logging
|
|
192
|
+
- `statsEnabled` - Enable proof of play ('1' or '0')
|
|
193
|
+
- `aggregationLevel` - Stats aggregation ('Individual' or 'Aggregate')
|
|
194
|
+
- `logLevel` - Log level ('error', 'audit', 'info', 'debug')
|
|
195
|
+
|
|
196
|
+
### XMR
|
|
197
|
+
- `xmrNetworkAddress` - XMR TCP address (e.g., 'tcp://xmr.example.com:9505')
|
|
198
|
+
- `xmrWebSocketAddress` - XMR WebSocket address (e.g., 'ws://xmr.example.com:9505')
|
|
199
|
+
- `xmrCmsKey` - XMR encryption key
|
|
200
|
+
|
|
201
|
+
### Features
|
|
202
|
+
- `preventSleep` - Prevent display sleep (boolean, default: true)
|
|
203
|
+
- `embeddedServerPort` - Embedded server port (default: 9696)
|
|
204
|
+
- `screenshotInterval` - Screenshot interval in seconds (default: 120)
|
|
205
|
+
|
|
206
|
+
### Download Windows
|
|
207
|
+
- `downloadStartWindow` - Download start time ('HH:MM')
|
|
208
|
+
- `downloadEndWindow` - Download end time ('HH:MM')
|
|
209
|
+
|
|
210
|
+
### Other
|
|
211
|
+
- `licenceCode` - Commercial license code
|
|
212
|
+
- `isSspEnabled` - Enable SSP ad space (boolean)
|
|
213
|
+
|
|
214
|
+
## Setting Name Formats
|
|
215
|
+
|
|
216
|
+
Supports both lowercase and CamelCase (uppercase first letter):
|
|
217
|
+
|
|
218
|
+
```javascript
|
|
219
|
+
// Both formats work
|
|
220
|
+
displaySettings.applySettings({
|
|
221
|
+
collectInterval: 600, // lowercase
|
|
222
|
+
CollectInterval: 600 // CamelCase
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
displaySettings.applySettings({
|
|
226
|
+
displayName: 'Test', // lowercase
|
|
227
|
+
DisplayName: 'Test' // CamelCase
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Download Windows
|
|
232
|
+
|
|
233
|
+
### Same-Day Window
|
|
234
|
+
|
|
235
|
+
```javascript
|
|
236
|
+
displaySettings.applySettings({
|
|
237
|
+
downloadStartWindow: '09:00',
|
|
238
|
+
downloadEndWindow: '17:00'
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Downloads allowed 09:00-17:00
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Overnight Window
|
|
245
|
+
|
|
246
|
+
```javascript
|
|
247
|
+
displaySettings.applySettings({
|
|
248
|
+
downloadStartWindow: '22:00',
|
|
249
|
+
downloadEndWindow: '06:00'
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Downloads allowed 22:00-23:59 and 00:00-06:00
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Events
|
|
256
|
+
|
|
257
|
+
### `interval-changed`
|
|
258
|
+
|
|
259
|
+
Emitted when collection interval changes.
|
|
260
|
+
|
|
261
|
+
**Callback:** `(newInterval: number) => void`
|
|
262
|
+
|
|
263
|
+
### `settings-applied`
|
|
264
|
+
|
|
265
|
+
Emitted when settings are applied.
|
|
266
|
+
|
|
267
|
+
**Callback:** `(settings: Object, changes: Array<string>) => void`
|
|
268
|
+
|
|
269
|
+
## Testing
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
npm test
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The test suite includes:
|
|
276
|
+
- 44 comprehensive test cases
|
|
277
|
+
- Constructor defaults
|
|
278
|
+
- Setting parsing and validation
|
|
279
|
+
- Collection interval enforcement (60s-24h)
|
|
280
|
+
- Boolean parsing ('1', '0', true, false)
|
|
281
|
+
- Download window logic (same-day and overnight)
|
|
282
|
+
- Screenshot interval tracking
|
|
283
|
+
- Edge cases and error handling
|
|
284
|
+
|
|
285
|
+
## Integration with Upstream
|
|
286
|
+
|
|
287
|
+
Based on upstream electron-player implementation:
|
|
288
|
+
- `/upstream_players/electron-player/src/main/config/config.ts`
|
|
289
|
+
- `/upstream_players/electron-player/src/main/xmds/response/registerDisplay.ts`
|
|
290
|
+
|
|
291
|
+
## License
|
|
292
|
+
|
|
293
|
+
AGPL-3.0-or-later
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xiboplayer/settings",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "CMS settings management and application for Xibo Player",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./manager": "./src/settings.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"test:watch": "vitest",
|
|
14
|
+
"test:coverage": "vitest run --coverage"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@xiboplayer/utils": "file:../utils"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"jsdom": "^25.0.1",
|
|
21
|
+
"vitest": "^2.0.0"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"xibo",
|
|
25
|
+
"digital-signage",
|
|
26
|
+
"settings",
|
|
27
|
+
"configuration",
|
|
28
|
+
"display-settings"
|
|
29
|
+
],
|
|
30
|
+
"author": "Pau Aliagas <linuxnow@gmail.com>",
|
|
31
|
+
"license": "AGPL-3.0-or-later",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/xibo/xibo-players.git",
|
|
35
|
+
"directory": "packages/settings"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/index.js
ADDED
package/src/settings.js
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DisplaySettings - CMS display settings management
|
|
3
|
+
*
|
|
4
|
+
* Parses and applies configuration from RegisterDisplay response.
|
|
5
|
+
* Based on upstream electron-player implementation.
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* ┌─────────────────────────────────────────────────────┐
|
|
9
|
+
* │ PlayerCore │
|
|
10
|
+
* │ - Receives RegisterDisplay response │
|
|
11
|
+
* │ - Passes to DisplaySettings.applySettings() │
|
|
12
|
+
* └─────────────────────────────────────────────────────┘
|
|
13
|
+
* ↓
|
|
14
|
+
* ┌─────────────────────────────────────────────────────┐
|
|
15
|
+
* │ DisplaySettings (this module) │
|
|
16
|
+
* │ - Parse all CMS settings │
|
|
17
|
+
* │ - Validate and normalize values │
|
|
18
|
+
* │ - Apply collection interval │
|
|
19
|
+
* │ - Check download windows │
|
|
20
|
+
* │ - Handle screenshot requests │
|
|
21
|
+
* │ - Emit events on changes │
|
|
22
|
+
* └─────────────────────────────────────────────────────┘
|
|
23
|
+
* ↓
|
|
24
|
+
* ┌─────────────────────────────────────────────────────┐
|
|
25
|
+
* │ Platform Layer (PWA/Electron/Mobile) │
|
|
26
|
+
* │ - Listen for setting change events │
|
|
27
|
+
* │ - Update UI with display name │
|
|
28
|
+
* │ - Handle screenshot requests │
|
|
29
|
+
* │ - Respect download windows │
|
|
30
|
+
* └─────────────────────────────────────────────────────┘
|
|
31
|
+
*
|
|
32
|
+
* Usage:
|
|
33
|
+
* const settings = new DisplaySettings();
|
|
34
|
+
* settings.applySettings(regResult.settings);
|
|
35
|
+
*
|
|
36
|
+
* // Get settings
|
|
37
|
+
* const collectInterval = settings.getCollectInterval();
|
|
38
|
+
* const canDownload = settings.isInDownloadWindow();
|
|
39
|
+
*
|
|
40
|
+
* // Listen for changes
|
|
41
|
+
* settings.on('interval-changed', (newInterval) => { ... });
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { EventEmitter } from '@xiboplayer/utils';
|
|
45
|
+
|
|
46
|
+
export class DisplaySettings extends EventEmitter {
|
|
47
|
+
constructor() {
|
|
48
|
+
super();
|
|
49
|
+
|
|
50
|
+
// Current settings (with defaults)
|
|
51
|
+
this.settings = {
|
|
52
|
+
// Collection
|
|
53
|
+
collectInterval: 300, // seconds (5 minutes default)
|
|
54
|
+
|
|
55
|
+
// Display info
|
|
56
|
+
displayName: 'Unknown Display',
|
|
57
|
+
sizeX: 1920,
|
|
58
|
+
sizeY: 1080,
|
|
59
|
+
|
|
60
|
+
// Stats
|
|
61
|
+
statsEnabled: false,
|
|
62
|
+
aggregationLevel: 'Individual', // or 'Aggregate'
|
|
63
|
+
|
|
64
|
+
// Logging
|
|
65
|
+
logLevel: 'error', // 'error', 'audit', 'info', 'debug'
|
|
66
|
+
|
|
67
|
+
// XMR
|
|
68
|
+
xmrNetworkAddress: null,
|
|
69
|
+
xmrWebSocketAddress: null,
|
|
70
|
+
xmrCmsKey: null,
|
|
71
|
+
|
|
72
|
+
// Features
|
|
73
|
+
preventSleep: true,
|
|
74
|
+
embeddedServerPort: 9696,
|
|
75
|
+
screenshotInterval: 120, // seconds
|
|
76
|
+
|
|
77
|
+
// Download windows
|
|
78
|
+
downloadStartWindow: null,
|
|
79
|
+
downloadEndWindow: null,
|
|
80
|
+
|
|
81
|
+
// License
|
|
82
|
+
licenceCode: null,
|
|
83
|
+
|
|
84
|
+
// SSP (ad space)
|
|
85
|
+
isSspEnabled: false,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Apply settings from RegisterDisplay response
|
|
91
|
+
* @param {Object} settings - Raw settings from CMS
|
|
92
|
+
* @returns {Object} Applied settings with changes
|
|
93
|
+
*/
|
|
94
|
+
applySettings(settings) {
|
|
95
|
+
if (!settings) {
|
|
96
|
+
console.warn('[DisplaySettings] No settings provided');
|
|
97
|
+
return { changed: [], settings: this.settings };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const changes = [];
|
|
101
|
+
const oldInterval = this.settings.collectInterval;
|
|
102
|
+
|
|
103
|
+
// Parse all settings with defaults
|
|
104
|
+
// Handle both lowercase and CamelCase (uppercase first letter)
|
|
105
|
+
this.settings.collectInterval = this.parseCollectInterval(settings.collectInterval || settings.CollectInterval);
|
|
106
|
+
this.settings.displayName = settings.displayName || settings.DisplayName || this.settings.displayName;
|
|
107
|
+
this.settings.sizeX = parseInt(settings.sizeX || settings.SizeX || this.settings.sizeX);
|
|
108
|
+
this.settings.sizeY = parseInt(settings.sizeY || settings.SizeY || this.settings.sizeY);
|
|
109
|
+
|
|
110
|
+
// Stats
|
|
111
|
+
this.settings.statsEnabled = this.parseBoolean(settings.statsEnabled || settings.StatsEnabled);
|
|
112
|
+
this.settings.aggregationLevel = settings.aggregationLevel || settings.AggregationLevel || this.settings.aggregationLevel;
|
|
113
|
+
|
|
114
|
+
// Logging
|
|
115
|
+
this.settings.logLevel = settings.logLevel || settings.LogLevel || this.settings.logLevel;
|
|
116
|
+
|
|
117
|
+
// XMR
|
|
118
|
+
this.settings.xmrNetworkAddress = settings.xmrNetworkAddress || settings.XmrNetworkAddress || this.settings.xmrNetworkAddress;
|
|
119
|
+
this.settings.xmrWebSocketAddress = settings.xmrWebSocketAddress || settings.XmrWebSocketAddress || this.settings.xmrWebSocketAddress;
|
|
120
|
+
this.settings.xmrCmsKey = settings.xmrCmsKey || settings.XmrCmsKey || this.settings.xmrCmsKey;
|
|
121
|
+
|
|
122
|
+
// Features
|
|
123
|
+
this.settings.preventSleep = this.parseBoolean(settings.preventSleep || settings.PreventSleep, true);
|
|
124
|
+
this.settings.embeddedServerPort = parseInt(settings.embeddedServerPort || settings.EmbeddedServerPort || this.settings.embeddedServerPort);
|
|
125
|
+
this.settings.screenshotInterval = parseInt(settings.screenshotInterval || settings.ScreenshotInterval || this.settings.screenshotInterval);
|
|
126
|
+
|
|
127
|
+
// Download windows
|
|
128
|
+
this.settings.downloadStartWindow = settings.downloadStartWindow || settings.DownloadStartWindow || this.settings.downloadStartWindow;
|
|
129
|
+
this.settings.downloadEndWindow = settings.downloadEndWindow || settings.DownloadEndWindow || this.settings.downloadEndWindow;
|
|
130
|
+
|
|
131
|
+
// License
|
|
132
|
+
this.settings.licenceCode = settings.licenceCode || settings.LicenceCode || this.settings.licenceCode;
|
|
133
|
+
|
|
134
|
+
// SSP
|
|
135
|
+
this.settings.isSspEnabled = this.parseBoolean(settings.isAdspaceEnabled || settings.IsAdspaceEnabled);
|
|
136
|
+
|
|
137
|
+
// Detect changes
|
|
138
|
+
if (oldInterval !== this.settings.collectInterval) {
|
|
139
|
+
changes.push('collectInterval');
|
|
140
|
+
this.emit('interval-changed', this.settings.collectInterval);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Emit generic settings-applied event
|
|
144
|
+
this.emit('settings-applied', this.settings, changes);
|
|
145
|
+
|
|
146
|
+
console.log('[DisplaySettings] Applied settings:', {
|
|
147
|
+
collectInterval: this.settings.collectInterval,
|
|
148
|
+
displayName: this.settings.displayName,
|
|
149
|
+
statsEnabled: this.settings.statsEnabled,
|
|
150
|
+
changes
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return { changed: changes, settings: this.settings };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Parse collection interval (seconds)
|
|
158
|
+
* @param {*} value - Raw value from CMS
|
|
159
|
+
* @returns {number} Collection interval in seconds
|
|
160
|
+
*/
|
|
161
|
+
parseCollectInterval(value) {
|
|
162
|
+
const interval = parseInt(value, 10);
|
|
163
|
+
|
|
164
|
+
// Validate range (minimum 60s, maximum 86400s = 24h)
|
|
165
|
+
if (isNaN(interval) || interval < 60) {
|
|
166
|
+
return 300; // 5 minutes default
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (interval > 86400) {
|
|
170
|
+
return 86400; // 24 hours max
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return interval;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Parse boolean setting
|
|
178
|
+
* @param {*} value - Raw value from CMS (string '1' or '0', or boolean)
|
|
179
|
+
* @param {boolean} defaultValue - Default if not set
|
|
180
|
+
* @returns {boolean}
|
|
181
|
+
*/
|
|
182
|
+
parseBoolean(value, defaultValue = false) {
|
|
183
|
+
if (value === true || value === false) {
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (value === '1' || value === 1) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (value === '0' || value === 0) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return defaultValue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get collection interval in seconds
|
|
200
|
+
* @returns {number}
|
|
201
|
+
*/
|
|
202
|
+
getCollectInterval() {
|
|
203
|
+
return this.settings.collectInterval;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get display name
|
|
208
|
+
* @returns {string}
|
|
209
|
+
*/
|
|
210
|
+
getDisplayName() {
|
|
211
|
+
return this.settings.displayName;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get display size
|
|
216
|
+
* @returns {{ width: number, height: number }}
|
|
217
|
+
*/
|
|
218
|
+
getDisplaySize() {
|
|
219
|
+
return {
|
|
220
|
+
width: this.settings.sizeX,
|
|
221
|
+
height: this.settings.sizeY
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Check if stats are enabled
|
|
227
|
+
* @returns {boolean}
|
|
228
|
+
*/
|
|
229
|
+
isStatsEnabled() {
|
|
230
|
+
return this.settings.statsEnabled;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get all settings
|
|
235
|
+
* @returns {Object}
|
|
236
|
+
*/
|
|
237
|
+
getAllSettings() {
|
|
238
|
+
return { ...this.settings };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get a specific setting by key
|
|
243
|
+
* @param {string} key - Setting key
|
|
244
|
+
* @param {*} defaultValue - Default value if not set
|
|
245
|
+
* @returns {*}
|
|
246
|
+
*/
|
|
247
|
+
getSetting(key, defaultValue = null) {
|
|
248
|
+
return this.settings[key] !== undefined ? this.settings[key] : defaultValue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Check if current time is within download window
|
|
253
|
+
* @returns {boolean}
|
|
254
|
+
*/
|
|
255
|
+
isInDownloadWindow() {
|
|
256
|
+
// If no download window configured, always allow
|
|
257
|
+
if (!this.settings.downloadStartWindow || !this.settings.downloadEndWindow) {
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const now = new Date();
|
|
263
|
+
const currentTime = now.getHours() * 60 + now.getMinutes();
|
|
264
|
+
|
|
265
|
+
const start = this.parseTimeWindow(this.settings.downloadStartWindow);
|
|
266
|
+
const end = this.parseTimeWindow(this.settings.downloadEndWindow);
|
|
267
|
+
|
|
268
|
+
// Handle overnight window (e.g., 22:00 - 06:00)
|
|
269
|
+
if (start > end) {
|
|
270
|
+
// Overnight: allow if AFTER start OR BEFORE end
|
|
271
|
+
return currentTime >= start || currentTime < end;
|
|
272
|
+
} else {
|
|
273
|
+
// Same day: allow if AFTER start AND BEFORE end
|
|
274
|
+
return currentTime >= start && currentTime < end;
|
|
275
|
+
}
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.warn('[DisplaySettings] Failed to parse download window:', error);
|
|
278
|
+
return true; // Allow downloads if parsing fails
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Parse time window string to minutes since midnight
|
|
284
|
+
* @param {string} timeStr - Time string (e.g., "14:30", "22:00")
|
|
285
|
+
* @returns {number} Minutes since midnight
|
|
286
|
+
*/
|
|
287
|
+
parseTimeWindow(timeStr) {
|
|
288
|
+
if (!timeStr || typeof timeStr !== 'string') {
|
|
289
|
+
throw new Error('Invalid time window format');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const parts = timeStr.split(':');
|
|
293
|
+
if (parts.length !== 2) {
|
|
294
|
+
throw new Error('Invalid time window format (expected HH:MM)');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const hours = parseInt(parts[0], 10);
|
|
298
|
+
const minutes = parseInt(parts[1], 10);
|
|
299
|
+
|
|
300
|
+
if (isNaN(hours) || isNaN(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
|
|
301
|
+
throw new Error('Invalid time window values');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return hours * 60 + minutes;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Get next download window start time
|
|
309
|
+
* @returns {Date|null} Next window start, or null if always allowed
|
|
310
|
+
*/
|
|
311
|
+
getNextDownloadWindow() {
|
|
312
|
+
if (!this.settings.downloadStartWindow || !this.settings.downloadEndWindow) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
const now = new Date();
|
|
318
|
+
const currentTime = now.getHours() * 60 + now.getMinutes();
|
|
319
|
+
const start = this.parseTimeWindow(this.settings.downloadStartWindow);
|
|
320
|
+
|
|
321
|
+
const nextWindow = new Date(now);
|
|
322
|
+
|
|
323
|
+
if (currentTime < start) {
|
|
324
|
+
// Window is later today
|
|
325
|
+
nextWindow.setHours(Math.floor(start / 60), start % 60, 0, 0);
|
|
326
|
+
} else {
|
|
327
|
+
// Window is tomorrow
|
|
328
|
+
nextWindow.setDate(nextWindow.getDate() + 1);
|
|
329
|
+
nextWindow.setHours(Math.floor(start / 60), start % 60, 0, 0);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return nextWindow;
|
|
333
|
+
} catch (error) {
|
|
334
|
+
console.warn('[DisplaySettings] Failed to calculate next download window:', error);
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Check if screenshot interval has elapsed
|
|
341
|
+
* @param {Date} lastScreenshot - Last screenshot timestamp
|
|
342
|
+
* @returns {boolean}
|
|
343
|
+
*/
|
|
344
|
+
shouldTakeScreenshot(lastScreenshot) {
|
|
345
|
+
if (!lastScreenshot) {
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const elapsed = (Date.now() - lastScreenshot.getTime()) / 1000;
|
|
350
|
+
return elapsed >= this.settings.screenshotInterval;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DisplaySettings tests
|
|
3
|
+
* Comprehensive test suite covering all settings parsing and validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
7
|
+
import { DisplaySettings } from './settings.js';
|
|
8
|
+
|
|
9
|
+
describe('DisplaySettings', () => {
|
|
10
|
+
let settings;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
settings = new DisplaySettings();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('constructor', () => {
|
|
17
|
+
it('should initialize with default settings', () => {
|
|
18
|
+
expect(settings.settings.collectInterval).toBe(300);
|
|
19
|
+
expect(settings.settings.displayName).toBe('Unknown Display');
|
|
20
|
+
expect(settings.settings.sizeX).toBe(1920);
|
|
21
|
+
expect(settings.settings.sizeY).toBe(1080);
|
|
22
|
+
expect(settings.settings.statsEnabled).toBe(false);
|
|
23
|
+
expect(settings.settings.logLevel).toBe('error');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('applySettings', () => {
|
|
28
|
+
it('should handle null settings gracefully', () => {
|
|
29
|
+
const result = settings.applySettings(null);
|
|
30
|
+
expect(result.changed).toEqual([]);
|
|
31
|
+
expect(result.settings.collectInterval).toBe(300);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should apply all settings from CMS response', () => {
|
|
35
|
+
const cmsSettings = {
|
|
36
|
+
collectInterval: 600,
|
|
37
|
+
displayName: 'Test Display',
|
|
38
|
+
sizeX: '3840',
|
|
39
|
+
sizeY: '2160',
|
|
40
|
+
statsEnabled: '1',
|
|
41
|
+
logLevel: 'debug',
|
|
42
|
+
xmrWebSocketAddress: 'ws://xmr.example.com:9505',
|
|
43
|
+
preventSleep: '1',
|
|
44
|
+
screenshotInterval: '180'
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const result = settings.applySettings(cmsSettings);
|
|
48
|
+
|
|
49
|
+
expect(result.settings.collectInterval).toBe(600);
|
|
50
|
+
expect(result.settings.displayName).toBe('Test Display');
|
|
51
|
+
expect(result.settings.sizeX).toBe(3840);
|
|
52
|
+
expect(result.settings.sizeY).toBe(2160);
|
|
53
|
+
expect(result.settings.statsEnabled).toBe(true);
|
|
54
|
+
expect(result.settings.logLevel).toBe('debug');
|
|
55
|
+
expect(result.settings.xmrWebSocketAddress).toBe('ws://xmr.example.com:9505');
|
|
56
|
+
expect(result.settings.preventSleep).toBe(true);
|
|
57
|
+
expect(result.settings.screenshotInterval).toBe(180);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should detect collection interval changes', () => {
|
|
61
|
+
const listener = vi.fn();
|
|
62
|
+
settings.on('interval-changed', listener);
|
|
63
|
+
|
|
64
|
+
settings.applySettings({ collectInterval: 600 });
|
|
65
|
+
|
|
66
|
+
expect(listener).toHaveBeenCalledWith(600);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should emit settings-applied event', () => {
|
|
70
|
+
const listener = vi.fn();
|
|
71
|
+
settings.on('settings-applied', listener);
|
|
72
|
+
|
|
73
|
+
const cmsSettings = { collectInterval: 600 };
|
|
74
|
+
settings.applySettings(cmsSettings);
|
|
75
|
+
|
|
76
|
+
expect(listener).toHaveBeenCalled();
|
|
77
|
+
expect(listener.mock.calls[0][1]).toContain('collectInterval');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should handle CamelCase setting names (uppercase first letter)', () => {
|
|
81
|
+
const cmsSettings = {
|
|
82
|
+
CollectInterval: 600,
|
|
83
|
+
DisplayName: 'CamelCase Display',
|
|
84
|
+
SizeX: '1920',
|
|
85
|
+
SizeY: '1080',
|
|
86
|
+
StatsEnabled: '1',
|
|
87
|
+
LogLevel: 'info'
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
settings.applySettings(cmsSettings);
|
|
91
|
+
|
|
92
|
+
expect(settings.settings.collectInterval).toBe(600);
|
|
93
|
+
expect(settings.settings.displayName).toBe('CamelCase Display');
|
|
94
|
+
expect(settings.settings.sizeX).toBe(1920);
|
|
95
|
+
expect(settings.settings.sizeY).toBe(1080);
|
|
96
|
+
expect(settings.settings.statsEnabled).toBe(true);
|
|
97
|
+
expect(settings.settings.logLevel).toBe('info');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('parseCollectInterval', () => {
|
|
102
|
+
it('should parse valid interval', () => {
|
|
103
|
+
expect(settings.parseCollectInterval(600)).toBe(600);
|
|
104
|
+
expect(settings.parseCollectInterval('900')).toBe(900);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should enforce minimum interval (60 seconds)', () => {
|
|
108
|
+
expect(settings.parseCollectInterval(30)).toBe(300);
|
|
109
|
+
expect(settings.parseCollectInterval(0)).toBe(300);
|
|
110
|
+
expect(settings.parseCollectInterval(-100)).toBe(300);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should enforce maximum interval (24 hours)', () => {
|
|
114
|
+
expect(settings.parseCollectInterval(100000)).toBe(86400);
|
|
115
|
+
expect(settings.parseCollectInterval(999999)).toBe(86400);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should handle invalid input', () => {
|
|
119
|
+
expect(settings.parseCollectInterval('invalid')).toBe(300);
|
|
120
|
+
expect(settings.parseCollectInterval(null)).toBe(300);
|
|
121
|
+
expect(settings.parseCollectInterval(undefined)).toBe(300);
|
|
122
|
+
expect(settings.parseCollectInterval(NaN)).toBe(300);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('parseBoolean', () => {
|
|
127
|
+
it('should parse boolean values', () => {
|
|
128
|
+
expect(settings.parseBoolean(true)).toBe(true);
|
|
129
|
+
expect(settings.parseBoolean(false)).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should parse string values', () => {
|
|
133
|
+
expect(settings.parseBoolean('1')).toBe(true);
|
|
134
|
+
expect(settings.parseBoolean('0')).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should parse numeric values', () => {
|
|
138
|
+
expect(settings.parseBoolean(1)).toBe(true);
|
|
139
|
+
expect(settings.parseBoolean(0)).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should use default value for invalid input', () => {
|
|
143
|
+
expect(settings.parseBoolean(null)).toBe(false);
|
|
144
|
+
expect(settings.parseBoolean(undefined)).toBe(false);
|
|
145
|
+
expect(settings.parseBoolean('yes')).toBe(false);
|
|
146
|
+
expect(settings.parseBoolean('no')).toBe(false);
|
|
147
|
+
expect(settings.parseBoolean(null, true)).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('getters', () => {
|
|
152
|
+
beforeEach(() => {
|
|
153
|
+
settings.applySettings({
|
|
154
|
+
collectInterval: 900,
|
|
155
|
+
displayName: 'My Display',
|
|
156
|
+
sizeX: '2560',
|
|
157
|
+
sizeY: '1440',
|
|
158
|
+
statsEnabled: '1'
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should get collection interval', () => {
|
|
163
|
+
expect(settings.getCollectInterval()).toBe(900);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should get display name', () => {
|
|
167
|
+
expect(settings.getDisplayName()).toBe('My Display');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should get display size', () => {
|
|
171
|
+
const size = settings.getDisplaySize();
|
|
172
|
+
expect(size.width).toBe(2560);
|
|
173
|
+
expect(size.height).toBe(1440);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should check if stats enabled', () => {
|
|
177
|
+
expect(settings.isStatsEnabled()).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should get all settings', () => {
|
|
181
|
+
const allSettings = settings.getAllSettings();
|
|
182
|
+
expect(allSettings.collectInterval).toBe(900);
|
|
183
|
+
expect(allSettings.displayName).toBe('My Display');
|
|
184
|
+
expect(allSettings.statsEnabled).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should get specific setting', () => {
|
|
188
|
+
expect(settings.getSetting('collectInterval')).toBe(900);
|
|
189
|
+
expect(settings.getSetting('displayName')).toBe('My Display');
|
|
190
|
+
expect(settings.getSetting('unknownSetting', 'default')).toBe('default');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('download windows', () => {
|
|
195
|
+
describe('isInDownloadWindow', () => {
|
|
196
|
+
it('should return true if no download window configured', () => {
|
|
197
|
+
expect(settings.isInDownloadWindow()).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should return true if only start window configured', () => {
|
|
201
|
+
settings.applySettings({ downloadStartWindow: '14:00' });
|
|
202
|
+
expect(settings.isInDownloadWindow()).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should return true if only end window configured', () => {
|
|
206
|
+
settings.applySettings({ downloadEndWindow: '18:00' });
|
|
207
|
+
expect(settings.isInDownloadWindow()).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should check normal same-day window', () => {
|
|
211
|
+
settings.applySettings({
|
|
212
|
+
downloadStartWindow: '09:00',
|
|
213
|
+
downloadEndWindow: '17:00'
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Mock current time to 12:00 (within window)
|
|
217
|
+
vi.useFakeTimers();
|
|
218
|
+
vi.setSystemTime(new Date(2026, 0, 15, 12, 0, 0));
|
|
219
|
+
|
|
220
|
+
expect(settings.isInDownloadWindow()).toBe(true);
|
|
221
|
+
|
|
222
|
+
// Mock current time to 08:00 (before window)
|
|
223
|
+
vi.setSystemTime(new Date(2026, 0, 15, 8, 0, 0));
|
|
224
|
+
expect(settings.isInDownloadWindow()).toBe(false);
|
|
225
|
+
|
|
226
|
+
// Mock current time to 18:00 (after window)
|
|
227
|
+
vi.setSystemTime(new Date(2026, 0, 15, 18, 0, 0));
|
|
228
|
+
expect(settings.isInDownloadWindow()).toBe(false);
|
|
229
|
+
|
|
230
|
+
vi.useRealTimers();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should check overnight window (crosses midnight)', () => {
|
|
234
|
+
settings.applySettings({
|
|
235
|
+
downloadStartWindow: '22:00',
|
|
236
|
+
downloadEndWindow: '06:00'
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Mock current time to 23:00 (within overnight window)
|
|
240
|
+
vi.useFakeTimers();
|
|
241
|
+
vi.setSystemTime(new Date(2026, 0, 15, 23, 0, 0));
|
|
242
|
+
|
|
243
|
+
expect(settings.isInDownloadWindow()).toBe(true);
|
|
244
|
+
|
|
245
|
+
// Mock current time to 02:00 (within overnight window)
|
|
246
|
+
vi.setSystemTime(new Date(2026, 0, 15, 2, 0, 0));
|
|
247
|
+
expect(settings.isInDownloadWindow()).toBe(true);
|
|
248
|
+
|
|
249
|
+
// Mock current time to 12:00 (outside overnight window)
|
|
250
|
+
vi.setSystemTime(new Date(2026, 0, 15, 12, 0, 0));
|
|
251
|
+
expect(settings.isInDownloadWindow()).toBe(false);
|
|
252
|
+
|
|
253
|
+
vi.useRealTimers();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should handle invalid time window gracefully', () => {
|
|
257
|
+
settings.applySettings({
|
|
258
|
+
downloadStartWindow: 'invalid',
|
|
259
|
+
downloadEndWindow: '17:00'
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Should return true (allow downloads) if parsing fails
|
|
263
|
+
expect(settings.isInDownloadWindow()).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('parseTimeWindow', () => {
|
|
268
|
+
it('should parse valid time strings', () => {
|
|
269
|
+
expect(settings.parseTimeWindow('00:00')).toBe(0);
|
|
270
|
+
expect(settings.parseTimeWindow('12:30')).toBe(750);
|
|
271
|
+
expect(settings.parseTimeWindow('23:59')).toBe(1439);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should reject invalid formats', () => {
|
|
275
|
+
expect(() => settings.parseTimeWindow('12')).toThrow('Invalid time window format');
|
|
276
|
+
expect(() => settings.parseTimeWindow('12:30:00')).toThrow('Invalid time window format');
|
|
277
|
+
expect(() => settings.parseTimeWindow('invalid')).toThrow('Invalid time window format');
|
|
278
|
+
expect(() => settings.parseTimeWindow(null)).toThrow('Invalid time window format');
|
|
279
|
+
expect(() => settings.parseTimeWindow(undefined)).toThrow('Invalid time window format');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should reject invalid values', () => {
|
|
283
|
+
expect(() => settings.parseTimeWindow('24:00')).toThrow('Invalid time window values');
|
|
284
|
+
expect(() => settings.parseTimeWindow('12:60')).toThrow('Invalid time window values');
|
|
285
|
+
expect(() => settings.parseTimeWindow('-1:30')).toThrow('Invalid time window values');
|
|
286
|
+
expect(() => settings.parseTimeWindow('12:-1')).toThrow('Invalid time window values');
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('getNextDownloadWindow', () => {
|
|
291
|
+
it('should return null if no download window configured', () => {
|
|
292
|
+
expect(settings.getNextDownloadWindow()).toBeNull();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('should calculate next window later today', () => {
|
|
296
|
+
settings.applySettings({
|
|
297
|
+
downloadStartWindow: '15:00',
|
|
298
|
+
downloadEndWindow: '18:00'
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Mock current time to 10:00
|
|
302
|
+
vi.useFakeTimers();
|
|
303
|
+
vi.setSystemTime(new Date(2026, 0, 15, 10, 0, 0));
|
|
304
|
+
|
|
305
|
+
const nextWindow = settings.getNextDownloadWindow();
|
|
306
|
+
expect(nextWindow).toBeTruthy();
|
|
307
|
+
expect(nextWindow.getHours()).toBe(15);
|
|
308
|
+
expect(nextWindow.getMinutes()).toBe(0);
|
|
309
|
+
|
|
310
|
+
vi.useRealTimers();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should calculate next window tomorrow', () => {
|
|
314
|
+
settings.applySettings({
|
|
315
|
+
downloadStartWindow: '09:00',
|
|
316
|
+
downloadEndWindow: '17:00'
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Mock current time to 20:00 (after window)
|
|
320
|
+
vi.useFakeTimers();
|
|
321
|
+
vi.setSystemTime(new Date(2026, 0, 15, 20, 0, 0));
|
|
322
|
+
|
|
323
|
+
const nextWindow = settings.getNextDownloadWindow();
|
|
324
|
+
expect(nextWindow).toBeTruthy();
|
|
325
|
+
expect(nextWindow.getDate()).toBe(16); // Tomorrow
|
|
326
|
+
expect(nextWindow.getHours()).toBe(9);
|
|
327
|
+
expect(nextWindow.getMinutes()).toBe(0);
|
|
328
|
+
|
|
329
|
+
vi.useRealTimers();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should handle invalid time window gracefully', () => {
|
|
333
|
+
settings.applySettings({
|
|
334
|
+
downloadStartWindow: 'invalid',
|
|
335
|
+
downloadEndWindow: '17:00'
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
expect(settings.getNextDownloadWindow()).toBeNull();
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe('screenshot', () => {
|
|
344
|
+
beforeEach(() => {
|
|
345
|
+
settings.applySettings({ screenshotInterval: 120 });
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should return true if no last screenshot', () => {
|
|
349
|
+
expect(settings.shouldTakeScreenshot(null)).toBe(true);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should return false if interval not elapsed', () => {
|
|
353
|
+
vi.useFakeTimers();
|
|
354
|
+
const now = new Date(2026, 0, 15, 12, 0, 0);
|
|
355
|
+
vi.setSystemTime(now);
|
|
356
|
+
|
|
357
|
+
const lastScreenshot = new Date(now.getTime() - 60000); // 60 seconds ago
|
|
358
|
+
expect(settings.shouldTakeScreenshot(lastScreenshot)).toBe(false);
|
|
359
|
+
|
|
360
|
+
vi.useRealTimers();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('should return true if interval elapsed', () => {
|
|
364
|
+
vi.useFakeTimers();
|
|
365
|
+
const now = new Date(2026, 0, 15, 12, 0, 0);
|
|
366
|
+
vi.setSystemTime(now);
|
|
367
|
+
|
|
368
|
+
const lastScreenshot = new Date(now.getTime() - 130000); // 130 seconds ago
|
|
369
|
+
expect(settings.shouldTakeScreenshot(lastScreenshot)).toBe(true);
|
|
370
|
+
|
|
371
|
+
vi.useRealTimers();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('should handle exact interval boundary', () => {
|
|
375
|
+
vi.useFakeTimers();
|
|
376
|
+
const now = new Date(2026, 0, 15, 12, 0, 0);
|
|
377
|
+
vi.setSystemTime(now);
|
|
378
|
+
|
|
379
|
+
const lastScreenshot = new Date(now.getTime() - 120000); // Exactly 120 seconds ago
|
|
380
|
+
expect(settings.shouldTakeScreenshot(lastScreenshot)).toBe(true);
|
|
381
|
+
|
|
382
|
+
vi.useRealTimers();
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe('edge cases and integration', () => {
|
|
387
|
+
it('should handle empty settings object', () => {
|
|
388
|
+
const result = settings.applySettings({});
|
|
389
|
+
expect(result.changed).toEqual([]);
|
|
390
|
+
expect(result.settings.collectInterval).toBe(300); // Default
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('should handle mixed case and missing values', () => {
|
|
394
|
+
const cmsSettings = {
|
|
395
|
+
CollectInterval: '450',
|
|
396
|
+
displayName: null,
|
|
397
|
+
SizeX: undefined,
|
|
398
|
+
statsEnabled: ''
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
settings.applySettings(cmsSettings);
|
|
402
|
+
|
|
403
|
+
expect(settings.settings.collectInterval).toBe(450);
|
|
404
|
+
expect(settings.settings.displayName).toBe('Unknown Display'); // Default
|
|
405
|
+
expect(settings.settings.sizeX).toBe(1920); // Default
|
|
406
|
+
expect(settings.settings.statsEnabled).toBe(false); // Default
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('should handle multiple consecutive applies', () => {
|
|
410
|
+
// First apply
|
|
411
|
+
settings.applySettings({ collectInterval: 600, displayName: 'Display 1' });
|
|
412
|
+
expect(settings.settings.collectInterval).toBe(600);
|
|
413
|
+
expect(settings.settings.displayName).toBe('Display 1');
|
|
414
|
+
|
|
415
|
+
// Second apply (different interval)
|
|
416
|
+
settings.applySettings({ collectInterval: 900, displayName: 'Display 2' });
|
|
417
|
+
expect(settings.settings.collectInterval).toBe(900);
|
|
418
|
+
expect(settings.settings.displayName).toBe('Display 2');
|
|
419
|
+
|
|
420
|
+
// Third apply (same interval, different name)
|
|
421
|
+
settings.applySettings({ collectInterval: 900, displayName: 'Display 3' });
|
|
422
|
+
expect(settings.settings.collectInterval).toBe(900);
|
|
423
|
+
expect(settings.settings.displayName).toBe('Display 3');
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('should preserve settings across multiple applies', () => {
|
|
427
|
+
// Apply initial settings
|
|
428
|
+
settings.applySettings({
|
|
429
|
+
collectInterval: 600,
|
|
430
|
+
displayName: 'Test',
|
|
431
|
+
statsEnabled: '1'
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
expect(settings.settings.statsEnabled).toBe(true);
|
|
435
|
+
expect(settings.settings.displayName).toBe('Test');
|
|
436
|
+
|
|
437
|
+
// Apply partial update (NOTE: settings are re-applied from scratch, not preserved)
|
|
438
|
+
// This matches upstream behavior where each RegisterDisplay overwrites settings
|
|
439
|
+
settings.applySettings({ collectInterval: 900 });
|
|
440
|
+
|
|
441
|
+
// After partial update, missing values revert to defaults (not preserved)
|
|
442
|
+
expect(settings.settings.statsEnabled).toBe(false); // Reverts to default
|
|
443
|
+
expect(settings.settings.collectInterval).toBe(900);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('should handle SSP (ad space) settings', () => {
|
|
447
|
+
settings.applySettings({ isAdspaceEnabled: '1' });
|
|
448
|
+
expect(settings.settings.isSspEnabled).toBe(true);
|
|
449
|
+
|
|
450
|
+
settings.applySettings({ isAdspaceEnabled: '0' });
|
|
451
|
+
expect(settings.settings.isSspEnabled).toBe(false);
|
|
452
|
+
|
|
453
|
+
settings.applySettings({ IsAdspaceEnabled: '1' });
|
|
454
|
+
expect(settings.settings.isSspEnabled).toBe(true);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('should handle XMR settings', () => {
|
|
458
|
+
const cmsSettings = {
|
|
459
|
+
xmrNetworkAddress: 'tcp://xmr.example.com:9505',
|
|
460
|
+
xmrWebSocketAddress: 'ws://xmr.example.com:9505',
|
|
461
|
+
xmrCmsKey: 'test-key-12345'
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
settings.applySettings(cmsSettings);
|
|
465
|
+
|
|
466
|
+
expect(settings.settings.xmrNetworkAddress).toBe('tcp://xmr.example.com:9505');
|
|
467
|
+
expect(settings.settings.xmrWebSocketAddress).toBe('ws://xmr.example.com:9505');
|
|
468
|
+
expect(settings.settings.xmrCmsKey).toBe('test-key-12345');
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('should handle all supported log levels', () => {
|
|
472
|
+
const levels = ['error', 'audit', 'info', 'debug'];
|
|
473
|
+
|
|
474
|
+
for (const level of levels) {
|
|
475
|
+
settings.applySettings({ logLevel: level });
|
|
476
|
+
expect(settings.settings.logLevel).toBe(level);
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
});
|
package/vitest.config.js
ADDED
package/vitest.setup.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest setup file
|
|
3
|
+
* Mocks browser APIs that are not available in test environment
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Mock localStorage
|
|
7
|
+
const localStorageMock = {
|
|
8
|
+
store: {},
|
|
9
|
+
getItem(key) {
|
|
10
|
+
return this.store[key] || null;
|
|
11
|
+
},
|
|
12
|
+
setItem(key, value) {
|
|
13
|
+
this.store[key] = String(value);
|
|
14
|
+
},
|
|
15
|
+
removeItem(key) {
|
|
16
|
+
delete this.store[key];
|
|
17
|
+
},
|
|
18
|
+
clear() {
|
|
19
|
+
this.store = {};
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
global.localStorage = localStorageMock;
|