@raydiant/playlist-window-manager 0.29.1 → 0.30.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.
@@ -62,12 +62,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
62
62
  return (mod && mod.__esModule) ? mod : { "default": mod };
63
63
  };
64
64
  Object.defineProperty(exports, "__esModule", { value: true });
65
- exports.createRenderer = exports.createLogger = exports.createRuleEngineState = exports.createPlaylistWindowManagerState = exports.createApplicationVersion = exports.createPresentationRelationships = exports.createPresentation = exports.createPresentationWindow = void 0;
65
+ exports.flush = exports.createFakeSurfaceHost = exports.createRecordingRenderer = exports.createRenderer = exports.createLogger = exports.createRuleEngineState = exports.createPlaylistWindowManagerState = exports.createApplicationVersion = exports.createPresentationRelationships = exports.createPresentation = exports.createPresentationWindow = void 0;
66
66
  var re = __importStar(require("@raydiant/playlist-rule-engine"));
67
67
  var PresentationWindow_1 = __importDefault(require("./PresentationWindow"));
68
68
  var PlaylistWindowManagerState_1 = __importDefault(require("./PlaylistWindowManagerState"));
69
+ // Orders calls recorded by the renderer and the fake surface host against
70
+ // each other.
71
+ var sequence = 0;
72
+ var nextSequence = function () {
73
+ sequence += 1;
74
+ return sequence;
75
+ };
69
76
  function createPresentationWindow(overrides) {
70
- var _a, _b, _c, _d, _e, _f;
77
+ var _a, _b, _c, _d, _e, _f, _g;
71
78
  if (overrides === void 0) { overrides = {}; }
72
79
  return new PresentationWindow_1.default({
73
80
  windowId: (_a = overrides.windowId) !== null && _a !== void 0 ? _a : 'test',
@@ -76,9 +83,9 @@ function createPresentationWindow(overrides) {
76
83
  publishedAt: (_b = overrides.publishedAt) !== null && _b !== void 0 ? _b : 'publishedAt',
77
84
  playlistWindowManagerState: (_c = overrides.playlistWindowManagerState) !== null && _c !== void 0 ? _c : createPlaylistWindowManagerState(),
78
85
  ruleEngineState: (_d = overrides.ruleEngineState) !== null && _d !== void 0 ? _d : createRuleEngineState(),
79
- logger: createLogger(),
80
- workerState: (_e = overrides.workerState) !== null && _e !== void 0 ? _e : {},
81
- renderer: (_f = overrides.renderer) !== null && _f !== void 0 ? _f : {
86
+ logger: (_e = overrides.logger) !== null && _e !== void 0 ? _e : createLogger(),
87
+ workerState: (_f = overrides.workerState) !== null && _f !== void 0 ? _f : {},
88
+ renderer: (_g = overrides.renderer) !== null && _g !== void 0 ? _g : {
82
89
  createWindow: function () { },
83
90
  loadWindow: function () {
84
91
  return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) {
@@ -101,6 +108,7 @@ function createPresentationWindow(overrides) {
101
108
  getTerminal: overrides.getTerminal,
102
109
  isPreview: overrides.isPreview,
103
110
  device: overrides.device,
111
+ surfaceBroker: overrides.surfaceBroker,
104
112
  });
105
113
  }
106
114
  exports.createPresentationWindow = createPresentationWindow;
@@ -210,3 +218,317 @@ function createRenderer() {
210
218
  };
211
219
  }
212
220
  exports.createRenderer = createRenderer;
221
+ // A renderer that records every call in order, across every window, and lets
222
+ // a test play the app's side of the message channel through its hooks.
223
+ function createRecordingRenderer() {
224
+ var calls = [];
225
+ // Messages the apps sent the window manager, in the same order as calls.
226
+ var received = [];
227
+ var callbacks = {};
228
+ var errorCallbacks = {};
229
+ var hooks = {};
230
+ var record = function (method, windowId) {
231
+ var args = [];
232
+ for (var _i = 2; _i < arguments.length; _i++) {
233
+ args[_i - 2] = arguments[_i];
234
+ }
235
+ calls.push({ method: method, windowId: windowId, args: args, seq: nextSequence() });
236
+ };
237
+ var renderer = {
238
+ createWindow: function (opts) {
239
+ callbacks[opts.windowId] = opts.receiveMessageCallback || (function () { return null; });
240
+ errorCallbacks[opts.windowId] = opts.errorCallback;
241
+ record('createWindow', opts.windowId, opts.backgroundColor);
242
+ },
243
+ loadWindow: function (windowId) {
244
+ return __awaiter(this, void 0, void 0, function () {
245
+ return __generator(this, function (_a) {
246
+ record('loadWindow', windowId);
247
+ if (hooks.onLoad)
248
+ hooks.onLoad(windowId);
249
+ return [2 /*return*/];
250
+ });
251
+ });
252
+ },
253
+ updateWindowBounds: function (windowId, bounds) {
254
+ record('updateWindowBounds', windowId, bounds);
255
+ },
256
+ showWindow: function (windowId, isFocused) {
257
+ record('showWindow', windowId, isFocused);
258
+ },
259
+ hideWindow: function (windowId) {
260
+ record('hideWindow', windowId);
261
+ },
262
+ removeWindow: function (windowId) {
263
+ return __awaiter(this, void 0, void 0, function () {
264
+ return __generator(this, function (_a) {
265
+ record('removeWindow', windowId);
266
+ delete callbacks[windowId];
267
+ return [2 /*return*/];
268
+ });
269
+ });
270
+ },
271
+ sendMessageString: function (windowId, message) {
272
+ var _a = JSON.parse(message), type = _a.type, payload = _a.payload;
273
+ record('sendMessageString', windowId, type, payload);
274
+ if (hooks.onMessage)
275
+ hooks.onMessage(windowId, type, payload);
276
+ },
277
+ sendMessageObject: function (windowId, type, payload) {
278
+ record('sendMessageObject', windowId, type, payload);
279
+ },
280
+ muteWindow: function (windowId) {
281
+ record('muteWindow', windowId);
282
+ },
283
+ unMuteWindow: function (windowId) {
284
+ record('unMuteWindow', windowId);
285
+ },
286
+ setWindowBackground: function (windowId, color) {
287
+ record('setWindowBackground', windowId, color);
288
+ },
289
+ };
290
+ return {
291
+ renderer: renderer,
292
+ calls: calls,
293
+ received: received,
294
+ hooks: hooks,
295
+ // Deliver a message from the app in a window to the window manager.
296
+ receive: function (windowId, type, payload) {
297
+ var callback = callbacks[windowId];
298
+ if (!callback)
299
+ return;
300
+ received.push({
301
+ method: type,
302
+ windowId: windowId,
303
+ args: [payload],
304
+ seq: nextSequence(),
305
+ });
306
+ callback(type, payload);
307
+ },
308
+ // Report a window as crashed or unresponsive, as a renderer would.
309
+ fail: function (windowId, err) {
310
+ var callback = errorCallbacks[windowId];
311
+ if (callback && callbacks[windowId])
312
+ callback(err);
313
+ },
314
+ // Windows created and not yet removed, oldest first.
315
+ liveWindowIds: function () {
316
+ return Object.keys(callbacks);
317
+ },
318
+ };
319
+ }
320
+ exports.createRecordingRenderer = createRecordingRenderer;
321
+ // A stand-in for the Signage Stick host's video surface, typed to the video
322
+ // contract. It records every call in order and records a rule break, rather
323
+ // than throwing, so a test can play a whole playlist and then assert that no
324
+ // rule was broken.
325
+ function createFakeSurfaceHost(opts) {
326
+ var _a, _b, _c;
327
+ if (opts === void 0) { opts = {}; }
328
+ var decoders = (_a = opts.decoders) !== null && _a !== void 0 ? _a : 4;
329
+ var refuseNext = opts.refuse;
330
+ var hangNext = (_b = opts.hang) !== null && _b !== void 0 ? _b : false;
331
+ var hangShowNext = (_c = opts.hangShow) !== null && _c !== void 0 ? _c : false;
332
+ var calls = [];
333
+ var violations = [];
334
+ var states = {};
335
+ var hung = [];
336
+ var lastId = 0;
337
+ var record = function (method, args, id) {
338
+ calls.push({ id: id, method: method, args: args, seq: nextSequence() });
339
+ };
340
+ var emit = function (id, event) {
341
+ var state = states[id];
342
+ if (!state)
343
+ throw new Error("No fake surface ".concat(id));
344
+ if (event.type === 'first_frame')
345
+ state.firstFrame = true;
346
+ if (state.listener) {
347
+ state.listener(event);
348
+ }
349
+ else {
350
+ state.buffered.push(event);
351
+ }
352
+ };
353
+ var live = function () {
354
+ return Object.keys(states)
355
+ .map(function (key) { return states[Number(key)]; })
356
+ .filter(function (s) { return s.live; });
357
+ };
358
+ var checkBounds = function (bounds) {
359
+ var values = [bounds.x, bounds.y, bounds.width, bounds.height];
360
+ if (values.some(function (v) { return Math.round(v) !== v; })) {
361
+ violations.push("fractional bounds ".concat(JSON.stringify(bounds)));
362
+ }
363
+ };
364
+ var createSurface = function () {
365
+ lastId += 1;
366
+ var id = lastId;
367
+ var state = {
368
+ id: id,
369
+ live: true,
370
+ firstFrame: false,
371
+ muted: true,
372
+ shown: false,
373
+ listener: null,
374
+ buffered: [],
375
+ };
376
+ states[id] = state;
377
+ return {
378
+ id: id,
379
+ show: function (showOpts) {
380
+ record('show', [showOpts], id);
381
+ if (!state.live)
382
+ return Promise.resolve();
383
+ if (!state.firstFrame) {
384
+ violations.push("surface ".concat(id, " shown before first_frame"));
385
+ }
386
+ live()
387
+ .filter(function (s) { return s !== state && s.shown && !s.muted; })
388
+ .forEach(function (s) {
389
+ violations.push("surface ".concat(id, " shown before surface ").concat(s.id, " was muted"));
390
+ });
391
+ state.shown = true;
392
+ if (hangShowNext) {
393
+ hangShowNext = false;
394
+ return new Promise(function () { return undefined; });
395
+ }
396
+ return Promise.resolve();
397
+ },
398
+ hide: function (hideOpts) {
399
+ record('hide', [hideOpts], id);
400
+ state.shown = false;
401
+ return Promise.resolve();
402
+ },
403
+ play: function () {
404
+ record('play', [], id);
405
+ if (opts.playingAfterMs !== undefined) {
406
+ setTimeout(function () {
407
+ if (state.live)
408
+ emit(id, { type: 'playing' });
409
+ }, opts.playingAfterMs);
410
+ }
411
+ return Promise.resolve();
412
+ },
413
+ pause: function () {
414
+ record('pause', [], id);
415
+ return Promise.resolve();
416
+ },
417
+ seek: function (seconds) {
418
+ record('seek', [seconds], id);
419
+ return Promise.resolve();
420
+ },
421
+ setBounds: function (r) {
422
+ record('setBounds', [r], id);
423
+ checkBounds(r);
424
+ return Promise.resolve();
425
+ },
426
+ setMuted: function (m) {
427
+ record('setMuted', [m], id);
428
+ if (!m && state.live) {
429
+ live()
430
+ .filter(function (s) { return s !== state && !s.muted; })
431
+ .forEach(function (s) {
432
+ violations.push("surface ".concat(id, " unmuted while surface ").concat(s.id, " sounds"));
433
+ });
434
+ }
435
+ if (state.live)
436
+ state.muted = m;
437
+ return Promise.resolve();
438
+ },
439
+ close: function () {
440
+ record('close', [], id);
441
+ state.live = false;
442
+ return Promise.resolve();
443
+ },
444
+ onEvent: function (fn) {
445
+ state.listener = fn;
446
+ var buffered = state.buffered;
447
+ state.buffered = [];
448
+ buffered.forEach(function (e) { return fn(e); });
449
+ },
450
+ };
451
+ };
452
+ var surfaces = {
453
+ openVideo: function (videoOpts) {
454
+ record('openVideo', [videoOpts]);
455
+ checkBounds(videoOpts.bounds);
456
+ if (hangNext) {
457
+ hangNext = false;
458
+ return new Promise(function (resolve) {
459
+ hung.push(function () { return resolve(createSurface()); });
460
+ });
461
+ }
462
+ if (refuseNext) {
463
+ var reason = refuseNext;
464
+ refuseNext = undefined;
465
+ return Promise.reject(new Error(reason));
466
+ }
467
+ if (live().length >= decoders) {
468
+ return Promise.reject(new Error('no_decoder'));
469
+ }
470
+ if (live().length >= 2) {
471
+ violations.push('a third live surface');
472
+ }
473
+ var surface = createSurface();
474
+ calls[calls.length - 1].id = surface.id;
475
+ return Promise.resolve(surface);
476
+ },
477
+ onHole: function (open) {
478
+ record('onHole', [open]);
479
+ },
480
+ };
481
+ return {
482
+ surfaces: surfaces,
483
+ calls: calls,
484
+ violations: violations,
485
+ // Raises an event on a surface, as the host would.
486
+ emit: emit,
487
+ // Lets a hung open resolve, late.
488
+ releaseHung: function () {
489
+ hung.splice(0).forEach(function (release) { return release(); });
490
+ },
491
+ liveIds: function () {
492
+ return live().map(function (s) { return s.id; });
493
+ },
494
+ lastId: function () {
495
+ return lastId;
496
+ },
497
+ // The next open is refused, as the host would refuse it.
498
+ refuseNext: function (reason) {
499
+ refuseNext = reason;
500
+ },
501
+ hangNextOpen: function () {
502
+ hangNext = true;
503
+ },
504
+ hangNextShow: function () {
505
+ hangShowNext = true;
506
+ },
507
+ };
508
+ }
509
+ exports.createFakeSurfaceHost = createFakeSurfaceHost;
510
+ // Lets pending promise callbacks and zero-delay timers run.
511
+ function flush(times) {
512
+ if (times === void 0) { times = 5; }
513
+ return __awaiter(this, void 0, void 0, function () {
514
+ var i;
515
+ return __generator(this, function (_a) {
516
+ switch (_a.label) {
517
+ case 0:
518
+ i = 0;
519
+ _a.label = 1;
520
+ case 1:
521
+ if (!(i < times)) return [3 /*break*/, 4];
522
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 0); })];
523
+ case 2:
524
+ _a.sent();
525
+ _a.label = 3;
526
+ case 3:
527
+ i += 1;
528
+ return [3 /*break*/, 1];
529
+ case 4: return [2 /*return*/];
530
+ }
531
+ });
532
+ });
533
+ }
534
+ exports.flush = flush;
package/build/types.d.ts CHANGED
@@ -113,6 +113,7 @@ export interface PresentationWindowRenderer {
113
113
  muteWindow(windowId: string): void;
114
114
  unMuteWindow(windowId: string): void;
115
115
  sendOnlineStatus?(windowId: string, isOnline: boolean): void;
116
+ setWindowBackground?(windowId: string, color: string): void;
116
117
  }
117
118
  export interface OpenChildWindowOptions {
118
119
  windowId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raydiant/playlist-window-manager",
3
- "version": "0.29.1",
3
+ "version": "0.30.0",
4
4
  "description": "Window manager for playlist playback.",
5
5
  "main": "build/index.js",
6
6
  "scripts": {
@@ -24,6 +24,7 @@
24
24
  "@types/uuid": "8.3.0",
25
25
  "electron": "38.0.0",
26
26
  "jest": "^24.8.0",
27
+ "mockdate": "^3.0.2",
27
28
  "ts-jest": "^24.0.2",
28
29
  "typescript": "^4.1.2"
29
30
  }
@@ -0,0 +1,88 @@
1
+ import IFrameRenderer from './IFrameRenderer';
2
+
3
+ function setup() {
4
+ const container = document.createElement('div');
5
+ document.body.appendChild(container);
6
+ const renderer = new IFrameRenderer(container);
7
+ const received: Array<[string, unknown]> = [];
8
+ renderer.createWindow({
9
+ windowId: 'w1',
10
+ backgroundColor: '#000000',
11
+ receiveMessageCallback: (type, payload) => received.push([type, payload]),
12
+ errorCallback: () => undefined,
13
+ });
14
+ const frame = container.querySelector('iframe') as HTMLIFrameElement;
15
+
16
+ const post = (type: string, source: Window | null) => {
17
+ window.dispatchEvent(
18
+ new MessageEvent('message', {
19
+ data: `raydiant:${JSON.stringify({ windowId: 'w1', type })}`,
20
+ source,
21
+ })
22
+ );
23
+ };
24
+
25
+ const teardown = () => {
26
+ renderer.destroy();
27
+ document.body.removeChild(container);
28
+ };
29
+
30
+ return { renderer, frame, received, post, teardown };
31
+ }
32
+
33
+ describe('only the window’s own frame asks for a video surface', () => {
34
+ test.each(['open_video', 'video_bounds', 'video_hole_open', 'close_video'])(
35
+ "%s from another frame's source is dropped",
36
+ (type) => {
37
+ const { frame, received, post, teardown } = setup();
38
+ const other = document.createElement('iframe');
39
+ document.body.appendChild(other);
40
+
41
+ post(type, other.contentWindow);
42
+ expect(received).toEqual([]);
43
+
44
+ post(type, frame.contentWindow);
45
+ expect(received).toEqual([[type, undefined]]);
46
+
47
+ document.body.removeChild(other);
48
+ teardown();
49
+ }
50
+ );
51
+
52
+ test("every other message keeps today's routing by window id", () => {
53
+ const { received, post, teardown } = setup();
54
+
55
+ post('presentation_ready', null);
56
+ expect(received).toEqual([['presentation_ready', undefined]]);
57
+
58
+ teardown();
59
+ });
60
+ });
61
+
62
+ describe('the frame background', () => {
63
+ test('setWindowBackground sets the frame’s inline background', () => {
64
+ const { renderer, frame, teardown } = setup();
65
+ expect(frame.style.backgroundColor).toBe('rgb(0, 0, 0)');
66
+
67
+ renderer.setWindowBackground('w1', 'transparent');
68
+ expect(frame.style.backgroundColor).toBe('transparent');
69
+
70
+ renderer.setWindowBackground('w1', '#000000');
71
+ expect(frame.style.backgroundColor).toBe('rgb(0, 0, 0)');
72
+
73
+ teardown();
74
+ });
75
+ });
76
+
77
+ describe('no colour scheme', () => {
78
+ test('a created frame declares no color-scheme', () => {
79
+ const { frame, renderer, teardown } = setup();
80
+ renderer.showWindow('w1', false);
81
+ renderer.setWindowBackground('w1', 'transparent');
82
+
83
+ expect(frame.style.getPropertyValue('color-scheme')).toBe('');
84
+ expect(frame.getAttribute('style')).not.toMatch(/color-scheme/);
85
+
86
+ teardown();
87
+ });
88
+ });
@@ -3,6 +3,7 @@ import {
3
3
  CreateWindowOptions,
4
4
  WindowBounds,
5
5
  } from './types';
6
+ import { VIDEO_REQUESTS } from './surfaces';
6
7
 
7
8
  interface IFrameRendererWindow {
8
9
  frame: HTMLIFrameElement;
@@ -219,6 +220,17 @@ export default class IFrameRenderer implements PresentationWindowRenderer {
219
220
  // mute presentation properties.
220
221
  }
221
222
 
223
+ public setWindowBackground(windowId: string, color: string) {
224
+ const window = this.getWindow(windowId);
225
+ if (!window) {
226
+ throw new Error(
227
+ `Failed to set window background, missing window id '${windowId}'`
228
+ );
229
+ }
230
+
231
+ window.frame.style.backgroundColor = color;
232
+ }
233
+
222
234
  public destroy() {
223
235
  window.removeEventListener('message', this.globalMessageHandler, false);
224
236
  }
@@ -246,6 +258,15 @@ export default class IFrameRenderer implements PresentationWindowRenderer {
246
258
  );
247
259
  }
248
260
 
261
+ // A surface costs a hardware decoder, so only the window's own frame may
262
+ // ask for one; any frame can post here and name any window.
263
+ if (
264
+ VIDEO_REQUESTS.indexOf(type) !== -1 &&
265
+ event.source !== window.frame.contentWindow
266
+ ) {
267
+ return;
268
+ }
269
+
249
270
  if (window.receiveMessageCallback) {
250
271
  window.receiveMessageCallback(type, payload);
251
272
  }