@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.
@@ -0,0 +1,1763 @@
1
+ "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || function (mod) {
30
+ if (mod && mod.__esModule) return mod;
31
+ var result = {};
32
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
33
+ __setModuleDefault(result, mod);
34
+ return result;
35
+ };
36
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
37
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
38
+ return new (P || (P = Promise))(function (resolve, reject) {
39
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
40
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
41
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
42
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
43
+ });
44
+ };
45
+ var __generator = (this && this.__generator) || function (thisArg, body) {
46
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
47
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
48
+ function verb(n) { return function (v) { return step([n, v]); }; }
49
+ function step(op) {
50
+ if (f) throw new TypeError("Generator is already executing.");
51
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
52
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
53
+ if (y = 0, t) op = [op[0] & 2, t.value];
54
+ switch (op[0]) {
55
+ case 0: case 1: t = op; break;
56
+ case 4: _.label++; return { value: op[1], done: false };
57
+ case 5: _.label++; y = op[1]; op = [0]; continue;
58
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
59
+ default:
60
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
61
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
62
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
63
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
64
+ if (t[2]) _.ops.pop();
65
+ _.trys.pop(); continue;
66
+ }
67
+ op = body.call(thisArg, _);
68
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
69
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
70
+ }
71
+ };
72
+ var __importDefault = (this && this.__importDefault) || function (mod) {
73
+ return (mod && mod.__esModule) ? mod : { "default": mod };
74
+ };
75
+ Object.defineProperty(exports, "__esModule", { value: true });
76
+ var RE = __importStar(require("@raydiant/playlist-rule-engine"));
77
+ var mockdate_1 = __importDefault(require("mockdate"));
78
+ var PlaylistWindowManager_1 = __importDefault(require("./PlaylistWindowManager"));
79
+ var testUtils_1 = require("./testUtils");
80
+ var CLOSE_WINDOW_DELAY_MS = 50;
81
+ var wait = function (ms) { return new Promise(function (resolve) { return setTimeout(resolve, ms); }); };
82
+ // Long enough for every scripted app reply and any surface show deadline.
83
+ var settle = function () { return __awaiter(void 0, void 0, void 0, function () {
84
+ return __generator(this, function (_a) {
85
+ switch (_a.label) {
86
+ case 0: return [4 /*yield*/, (0, testUtils_1.flush)(20)];
87
+ case 1:
88
+ _a.sent();
89
+ return [4 /*yield*/, wait(CLOSE_WINDOW_DELAY_MS * 2)];
90
+ case 2:
91
+ _a.sent();
92
+ return [4 /*yield*/, (0, testUtils_1.flush)(20)];
93
+ case 3:
94
+ _a.sent();
95
+ return [2 /*return*/];
96
+ }
97
+ });
98
+ }); };
99
+ var now = Date.parse('2026-10-28T12:00:00Z');
100
+ // The manager throttles completes that arrive within three seconds of each
101
+ // other; moving the clock past that keeps every test on the unthrottled path.
102
+ var passTime = function () {
103
+ now += 5000;
104
+ mockdate_1.default.set(now);
105
+ };
106
+ beforeEach(function () {
107
+ mockdate_1.default.set(now);
108
+ });
109
+ afterEach(function () {
110
+ mockdate_1.default.reset();
111
+ });
112
+ function createManager(_a) {
113
+ var presentationIds = _a.presentationIds, _b = _a.options, options = _b === void 0 ? {} : _b;
114
+ var presentations = {};
115
+ var rePresentations = {};
116
+ presentationIds.forEach(function (id) {
117
+ presentations[id] = (0, testUtils_1.createPresentation)({
118
+ id: id,
119
+ applicationDeploymentId: 'app',
120
+ applicationVariables: {
121
+ content: { url: "https://cdn.test/".concat(id, ".mp4") },
122
+ muted: false,
123
+ },
124
+ });
125
+ rePresentations[id] = { id: id, applicationName: 'videos' };
126
+ });
127
+ var state = (0, testUtils_1.createPlaylistWindowManagerState)({
128
+ presentations: presentations,
129
+ applicationVersions: { app: (0, testUtils_1.createApplicationVersion)({ id: 'app' }) },
130
+ });
131
+ var reState = new RE.State({
132
+ main: {
133
+ id: 'main',
134
+ mode: RE.PlaylistMode.Sequential,
135
+ items: presentationIds.map(function (presentationId) { return ({ presentationId: presentationId }); }),
136
+ },
137
+ }, rePresentations, {});
138
+ var recording = (0, testUtils_1.createRecordingRenderer)();
139
+ var manager = new PlaylistWindowManager_1.default(__assign({ ruleEngine: new RE.RuleEngine('main', reState), state: state, logger: (0, testUtils_1.createLogger)(), closeWindowDelayMs: CLOSE_WINDOW_DELAY_MS }, options));
140
+ manager.setRenderer(recording.renderer);
141
+ return { manager: manager, recording: recording };
142
+ }
143
+ // Plays the app and app renderer the way a still-image app does: ready on
144
+ // render, and a first paint shortly after each play.
145
+ function scriptImageApp(recording) {
146
+ recording.hooks.onLoad = function (windowId) { return recording.receive(windowId, 'loaded'); };
147
+ recording.hooks.onMessage = function (windowId, type) {
148
+ if (type === 'render')
149
+ recording.receive(windowId, 'presentation_ready');
150
+ if (type === 'play') {
151
+ setTimeout(function () { return recording.receive(windowId, 'first_painted'); }, 0);
152
+ }
153
+ };
154
+ }
155
+ // The window most recently shown, which is the one playing.
156
+ function lastShown(recording) {
157
+ var shows = recording.calls.filter(function (c) { return c.method === 'showWindow'; });
158
+ return shows[shows.length - 1].windowId;
159
+ }
160
+ // Names windows w1, w2, … in creation order so a sequence reads the same on
161
+ // every run.
162
+ function describeCalls(recording) {
163
+ var aliases = {};
164
+ return recording.calls.map(function (_a) {
165
+ var method = _a.method, windowId = _a.windowId, args = _a.args;
166
+ if (!aliases[windowId]) {
167
+ aliases[windowId] = "w".concat(Object.keys(aliases).length + 1);
168
+ }
169
+ var detail = method === 'sendMessageString' || method === 'sendMessageObject'
170
+ ? " ".concat(args[0])
171
+ : '';
172
+ return "".concat(method, " ").concat(aliases[windowId]).concat(detail);
173
+ });
174
+ }
175
+ describe('no surface host', function () {
176
+ test('a two-presentation swap keeps the renderer call sequence', function () { return __awaiter(void 0, void 0, void 0, function () {
177
+ var _a, manager, recording, current;
178
+ return __generator(this, function (_b) {
179
+ switch (_b.label) {
180
+ case 0:
181
+ _a = createManager({
182
+ presentationIds: ['a', 'b'],
183
+ }), manager = _a.manager, recording = _a.recording;
184
+ scriptImageApp(recording);
185
+ return [4 /*yield*/, manager.load()];
186
+ case 1:
187
+ _b.sent();
188
+ return [4 /*yield*/, (0, testUtils_1.flush)()];
189
+ case 2:
190
+ _b.sent();
191
+ manager.show();
192
+ manager.play();
193
+ return [4 /*yield*/, (0, testUtils_1.flush)()];
194
+ case 3:
195
+ _b.sent();
196
+ current = lastShown(recording);
197
+ passTime();
198
+ recording.receive(current, 'presentation_complete');
199
+ return [4 /*yield*/, (0, testUtils_1.flush)()];
200
+ case 4:
201
+ _b.sent();
202
+ return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, CLOSE_WINDOW_DELAY_MS); })];
203
+ case 5:
204
+ _b.sent();
205
+ return [4 /*yield*/, (0, testUtils_1.flush)()];
206
+ case 6:
207
+ _b.sent();
208
+ expect(describeCalls(recording)).toMatchSnapshot();
209
+ return [4 /*yield*/, manager.destroy()];
210
+ case 7:
211
+ _b.sent();
212
+ return [2 /*return*/];
213
+ }
214
+ });
215
+ }); });
216
+ });
217
+ // Plays the Videos app and app renderer the way the Videos plan describes,
218
+ // replying a task later as postMessage does: ask for a surface on render when
219
+ // offered one, size the hole and become ready on the first frame, open the
220
+ // hole on video_shown, complete on ended, fall back to <video> on a refusal.
221
+ function scriptApps(recording, host, script) {
222
+ var images = script.images || [];
223
+ var holdFirstFrame = new Set(script.holdFirstFrame || []);
224
+ var holdHole = new Set(script.holdHole || []);
225
+ var preopenHole = new Set(script.preopenHole || []);
226
+ var presentationOf = {};
227
+ var isReady = {};
228
+ var reply = function (windowId, type, payload) {
229
+ setTimeout(function () { return recording.receive(windowId, type, payload); }, 0);
230
+ };
231
+ var ready = function (windowId) {
232
+ if (isReady[windowId])
233
+ return;
234
+ isReady[windowId] = true;
235
+ reply(windowId, 'presentation_ready');
236
+ };
237
+ var surfaceOf = function (presentationId) {
238
+ var opens = host.calls.filter(function (c) {
239
+ return c.method === 'openVideo' &&
240
+ c.args[0].label === presentationId &&
241
+ c.id !== undefined;
242
+ });
243
+ return opens[opens.length - 1].id;
244
+ };
245
+ var emitFirstFrame = function (presentationId) {
246
+ host.emit(surfaceOf(presentationId), {
247
+ type: 'first_frame',
248
+ width: 1920,
249
+ height: 1080,
250
+ durationS: 10,
251
+ });
252
+ };
253
+ recording.hooks.onLoad = function (windowId) { return reply(windowId, 'loaded'); };
254
+ recording.hooks.onMessage = function (windowId, type, payload) {
255
+ var presentationId = presentationOf[windowId];
256
+ switch (type) {
257
+ case 'render': {
258
+ var id = payload.presentation.id;
259
+ presentationOf[windowId] = id;
260
+ if (payload.nativeVideo && images.indexOf(id) === -1) {
261
+ reply(windowId, 'open_video', {
262
+ url: "https://cdn.test/".concat(id, ".mp4"),
263
+ bounds: { x: 0, y: 0, width: 1920, height: 1080 },
264
+ muted: false,
265
+ });
266
+ }
267
+ else {
268
+ ready(windowId);
269
+ }
270
+ break;
271
+ }
272
+ case 'video_refused':
273
+ ready(windowId);
274
+ break;
275
+ case 'video_opened':
276
+ if (!holdFirstFrame.has(presentationId)) {
277
+ setTimeout(function () { return emitFirstFrame(presentationId); }, 0);
278
+ }
279
+ break;
280
+ case 'video_event':
281
+ if (payload.type === 'first_frame') {
282
+ reply(windowId, 'video_bounds', {
283
+ bounds: { x: 0, y: 0, width: 1920, height: 1080 },
284
+ });
285
+ ready(windowId);
286
+ if (preopenHole.has(presentationId)) {
287
+ reply(windowId, 'video_hole_open');
288
+ }
289
+ }
290
+ if (payload.type === 'ended') {
291
+ reply(windowId, 'presentation_complete');
292
+ }
293
+ break;
294
+ case 'video_shown':
295
+ if (holdHole.has(presentationId)) {
296
+ holdHole.delete(presentationId);
297
+ }
298
+ else {
299
+ reply(windowId, 'video_hole_open');
300
+ }
301
+ break;
302
+ case 'play':
303
+ reply(windowId, 'first_painted');
304
+ break;
305
+ }
306
+ };
307
+ var windowOf = function (presentationId) {
308
+ var ids = Object.keys(presentationOf).filter(function (w) { return presentationOf[w] === presentationId; });
309
+ return ids[ids.length - 1];
310
+ };
311
+ return { presentationOf: presentationOf, surfaceOf: surfaceOf, emitFirstFrame: emitFirstFrame, windowOf: windowOf };
312
+ }
313
+ function createSurfaceManager(presentationIds, script, hostOpts, managerOptions, showAfterRevealMs) {
314
+ var _this = this;
315
+ if (script === void 0) { script = {}; }
316
+ if (managerOptions === void 0) { managerOptions = {}; }
317
+ var host = (0, testUtils_1.createFakeSurfaceHost)(hostOpts);
318
+ var warn = jest.fn();
319
+ var _a = createManager({
320
+ presentationIds: presentationIds,
321
+ options: __assign({ surfaces: __assign(__assign({}, host.surfaces), { showAfterRevealMs: showAfterRevealMs }), logger: (0, testUtils_1.createLogger)({ warn: warn }) }, managerOptions),
322
+ }), manager = _a.manager, recording = _a.recording;
323
+ var apps = scriptApps(recording, host, script);
324
+ // Every call and app message in order. A window and its surface are named
325
+ // by their presentation, and a later window of the same presentation gets
326
+ // a number: a, then a2.
327
+ var entries = function () {
328
+ var nameWith = function (counts, label) {
329
+ counts[label] = (counts[label] || 0) + 1;
330
+ return counts[label] === 1 ? label : "".concat(label).concat(counts[label]);
331
+ };
332
+ var windowCounts = {};
333
+ var windowName = {};
334
+ Object.keys(apps.presentationOf).forEach(function (windowId) {
335
+ windowName[windowId] = nameWith(windowCounts, apps.presentationOf[windowId]);
336
+ });
337
+ var surfaceCounts = {};
338
+ var labelOf = {};
339
+ host.calls.forEach(function (c) {
340
+ if (c.method === 'openVideo' && c.id !== undefined) {
341
+ labelOf[c.id] = nameWith(surfaceCounts, c.args[0].label);
342
+ }
343
+ });
344
+ var rendererEntries = recording.calls.map(function (c) {
345
+ var name = windowName[c.windowId] || '?';
346
+ var detail = c.method === 'sendMessageString' || c.method === 'setWindowBackground'
347
+ ? " ".concat(c.args[0])
348
+ : '';
349
+ return { seq: c.seq, text: "".concat(c.method, " ").concat(name).concat(detail) };
350
+ });
351
+ var appEntries = recording.received.map(function (c) { return ({
352
+ seq: c.seq,
353
+ text: "app ".concat(windowName[c.windowId] || '?', " ").concat(c.method),
354
+ }); });
355
+ var hostEntries = host.calls.map(function (c) {
356
+ var name = c.id !== undefined ? labelOf[c.id] : '';
357
+ var detail = c.method === 'setMuted' ? " ".concat(c.args[0]) : '';
358
+ var onHole = c.method === 'onHole' ? " ".concat(c.args[0]) : '';
359
+ return {
360
+ seq: c.seq,
361
+ text: "host ".concat(c.method).concat(name ? " ".concat(name) : '').concat(detail).concat(onHole),
362
+ };
363
+ });
364
+ return rendererEntries
365
+ .concat(appEntries, hostEntries)
366
+ .sort(function (x, y) { return x.seq - y.seq; });
367
+ };
368
+ var timeline = function () { return entries().map(function (e) { return e.text; }); };
369
+ var sent = function (presentationId, type) {
370
+ return recording.calls.filter(function (c) {
371
+ return c.method === 'sendMessageString' &&
372
+ c.args[0] === type &&
373
+ apps.presentationOf[c.windowId] === presentationId;
374
+ });
375
+ };
376
+ var hostCalls = function (method, presentationId) {
377
+ var ids = presentationId === undefined
378
+ ? null
379
+ : host.calls
380
+ .filter(function (c) {
381
+ return c.method === 'openVideo' &&
382
+ c.args[0].label === presentationId;
383
+ })
384
+ .map(function (c) { return c.id; });
385
+ return host.calls.filter(function (c) { return c.method === method && (ids === null || ids.indexOf(c.id) !== -1); });
386
+ };
387
+ var complete = function (presentationId) { return __awaiter(_this, void 0, void 0, function () {
388
+ return __generator(this, function (_a) {
389
+ switch (_a.label) {
390
+ case 0:
391
+ passTime();
392
+ recording.receive(apps.windowOf(presentationId), 'presentation_complete');
393
+ return [4 /*yield*/, settle()];
394
+ case 1:
395
+ _a.sent();
396
+ return [2 /*return*/];
397
+ }
398
+ });
399
+ }); };
400
+ var start = function () { return __awaiter(_this, void 0, void 0, function () {
401
+ return __generator(this, function (_a) {
402
+ switch (_a.label) {
403
+ case 0: return [4 /*yield*/, manager.load()];
404
+ case 1:
405
+ _a.sent();
406
+ manager.show();
407
+ manager.play();
408
+ return [4 /*yield*/, settle()];
409
+ case 2:
410
+ _a.sent();
411
+ return [2 /*return*/];
412
+ }
413
+ });
414
+ }); };
415
+ return {
416
+ manager: manager,
417
+ recording: recording,
418
+ host: host,
419
+ warn: warn,
420
+ apps: apps,
421
+ entries: entries,
422
+ timeline: timeline,
423
+ sent: sent,
424
+ hostCalls: hostCalls,
425
+ complete: complete,
426
+ start: start,
427
+ };
428
+ }
429
+ // The calls and messages a swap's order is read from.
430
+ var swapSteps = [
431
+ 'host setMuted',
432
+ 'muteWindow',
433
+ 'host show',
434
+ 'sendMessageString',
435
+ 'app',
436
+ 'setWindowBackground',
437
+ 'showWindow',
438
+ 'hideWindow',
439
+ 'host close',
440
+ 'removeWindow',
441
+ ];
442
+ // Leaves out the next window's preload, which runs alongside the swap, and
443
+ // messages that are not part of the swap's order.
444
+ var isSwapStep = function (line) {
445
+ return swapSteps.some(function (step) { return line.startsWith(step); }) &&
446
+ !/ (\?|[a-z]\d)( |$)/.test(line) &&
447
+ !/ (render|play|log_\w+|loaded|video_opened|video_event)$/.test(line) &&
448
+ !/app \S+ (presentation_ready|video_bounds|open_video)/.test(line);
449
+ };
450
+ describe('a manager given a surface host', function () {
451
+ test('an app asks through miraVideo and render carries nativeVideo', function () { return __awaiter(void 0, void 0, void 0, function () {
452
+ var _a, manager, sent, hostCalls, start;
453
+ return __generator(this, function (_b) {
454
+ switch (_b.label) {
455
+ case 0:
456
+ _a = createSurfaceManager(['a']), manager = _a.manager, sent = _a.sent, hostCalls = _a.hostCalls, start = _a.start;
457
+ return [4 /*yield*/, start()];
458
+ case 1:
459
+ _b.sent();
460
+ expect(sent('a', 'render')[0].args[1]).toMatchObject({
461
+ nativeVideo: true,
462
+ });
463
+ expect(hostCalls('openVideo')).toHaveLength(1);
464
+ expect(hostCalls('openVideo')[0].args[0]).toEqual({
465
+ url: 'https://cdn.test/a.mp4',
466
+ bounds: { x: 0, y: 0, width: 1920, height: 1080 },
467
+ muted: true,
468
+ label: 'a',
469
+ });
470
+ return [4 /*yield*/, manager.destroy()];
471
+ case 2:
472
+ _b.sent();
473
+ return [2 /*return*/];
474
+ }
475
+ });
476
+ }); });
477
+ test('the first presentation shows its surface before its frame, and plays after the hole opens', function () { return __awaiter(void 0, void 0, void 0, function () {
478
+ var _a, manager, host, timeline, start, steps;
479
+ return __generator(this, function (_b) {
480
+ switch (_b.label) {
481
+ case 0:
482
+ _a = createSurfaceManager(['a']), manager = _a.manager, host = _a.host, timeline = _a.timeline, start = _a.start;
483
+ return [4 /*yield*/, start()];
484
+ case 1:
485
+ _b.sent();
486
+ steps = timeline().filter(function (line) {
487
+ return line.startsWith('host show') ||
488
+ line.startsWith('host play') ||
489
+ line.startsWith('host onHole') ||
490
+ line.startsWith('showWindow') ||
491
+ line === 'sendMessageString a video_shown' ||
492
+ line === 'sendMessageString a play' ||
493
+ line === 'app a video_hole_open';
494
+ });
495
+ expect(steps).toEqual([
496
+ 'host show a',
497
+ 'sendMessageString a video_shown',
498
+ 'app a video_hole_open',
499
+ 'host onHole true',
500
+ 'showWindow a',
501
+ 'sendMessageString a play',
502
+ 'host play a',
503
+ ]);
504
+ expect(host.violations).toEqual([]);
505
+ return [4 /*yield*/, manager.destroy()];
506
+ case 2:
507
+ _b.sent();
508
+ return [2 /*return*/];
509
+ }
510
+ });
511
+ }); });
512
+ test('the next presentation preloads hidden and is ready on its first frame, then its bounds', function () { return __awaiter(void 0, void 0, void 0, function () {
513
+ var _a, manager, recording, host, apps, hostCalls, start, next;
514
+ return __generator(this, function (_b) {
515
+ switch (_b.label) {
516
+ case 0:
517
+ _a = createSurfaceManager(['a', 'b'], { holdFirstFrame: ['b'] }), manager = _a.manager, recording = _a.recording, host = _a.host, apps = _a.apps, hostCalls = _a.hostCalls, start = _a.start;
518
+ return [4 /*yield*/, start()];
519
+ case 1:
520
+ _b.sent();
521
+ next = manager.nextWindow;
522
+ expect(hostCalls('openVideo', 'b')).toHaveLength(1);
523
+ expect(next.isReady()).toBe(false);
524
+ // The first frame alone does not make it ready; the bounds that follow do.
525
+ recording.hooks.onMessage = (function (original) { return function (w, type, payload) {
526
+ if (type === 'video_event' && payload.type === 'first_frame')
527
+ return;
528
+ if (original)
529
+ original(w, type, payload);
530
+ }; })(recording.hooks.onMessage);
531
+ apps.emitFirstFrame('b');
532
+ return [4 /*yield*/, settle()];
533
+ case 2:
534
+ _b.sent();
535
+ expect(next.isReady()).toBe(false);
536
+ recording.receive(apps.windowOf('b'), 'video_bounds', {
537
+ bounds: { x: 0, y: 0.4, width: 1920.4, height: 1080 },
538
+ });
539
+ recording.receive(apps.windowOf('b'), 'presentation_ready');
540
+ expect(next.isReady()).toBe(true);
541
+ expect(hostCalls('show', 'b')).toHaveLength(0);
542
+ expect(hostCalls('setBounds', 'b')[0].args[0]).toEqual({
543
+ x: 0,
544
+ y: 0,
545
+ width: 1920,
546
+ height: 1080,
547
+ });
548
+ expect(host.violations).toEqual([]);
549
+ return [4 /*yield*/, manager.destroy()];
550
+ case 3:
551
+ _b.sent();
552
+ return [2 /*return*/];
553
+ }
554
+ });
555
+ }); });
556
+ test('a video to video swap shows the new before it hides the old', function () { return __awaiter(void 0, void 0, void 0, function () {
557
+ var _a, manager, recording, host, apps, timeline, complete, start, a, b, hiddenInRevealTask, showWindow, before, swap;
558
+ return __generator(this, function (_b) {
559
+ switch (_b.label) {
560
+ case 0:
561
+ _a = createSurfaceManager(['a', 'b']), manager = _a.manager, recording = _a.recording, host = _a.host, apps = _a.apps, timeline = _a.timeline, complete = _a.complete, start = _a.start;
562
+ return [4 /*yield*/, start()];
563
+ case 1:
564
+ _b.sent();
565
+ a = apps.windowOf('a');
566
+ b = apps.windowOf('b');
567
+ hiddenInRevealTask = null;
568
+ showWindow = recording.renderer.showWindow;
569
+ recording.renderer.showWindow = function (windowId, isFocused) {
570
+ showWindow(windowId, isFocused);
571
+ if (windowId === b) {
572
+ Promise.resolve().then(function () {
573
+ hiddenInRevealTask = recording.calls.some(function (c) { return c.method === 'hideWindow' && c.windowId === a; });
574
+ });
575
+ }
576
+ };
577
+ before = timeline().length;
578
+ return [4 /*yield*/, complete('a')];
579
+ case 2:
580
+ _b.sent();
581
+ swap = timeline().slice(before).filter(isSwapStep);
582
+ expect(swap).toEqual([
583
+ 'app a presentation_complete',
584
+ 'host setMuted a true',
585
+ 'host show b',
586
+ 'host setMuted b false',
587
+ 'sendMessageString b video_shown',
588
+ 'app b video_hole_open',
589
+ 'setWindowBackground b transparent',
590
+ 'showWindow b',
591
+ 'hideWindow a',
592
+ 'app b first_painted',
593
+ 'sendMessageString a video_closing',
594
+ 'host close a',
595
+ 'removeWindow a',
596
+ ]);
597
+ expect(hiddenInRevealTask).toBe(true);
598
+ expect(host.violations).toEqual([]);
599
+ return [4 /*yield*/, manager.destroy()];
600
+ case 3:
601
+ _b.sent();
602
+ return [2 /*return*/];
603
+ }
604
+ });
605
+ }); });
606
+ test('a video to image swap reveals the image at once and closes the surface after its paint', function () { return __awaiter(void 0, void 0, void 0, function () {
607
+ var _a, manager, host, timeline, complete, start, before, swap;
608
+ return __generator(this, function (_b) {
609
+ switch (_b.label) {
610
+ case 0:
611
+ _a = createSurfaceManager(['a', 'b'], { images: ['b'] }), manager = _a.manager, host = _a.host, timeline = _a.timeline, complete = _a.complete, start = _a.start;
612
+ return [4 /*yield*/, start()];
613
+ case 1:
614
+ _b.sent();
615
+ before = timeline().length;
616
+ return [4 /*yield*/, complete('a')];
617
+ case 2:
618
+ _b.sent();
619
+ swap = timeline().slice(before).filter(isSwapStep);
620
+ expect(swap).toEqual([
621
+ 'app a presentation_complete',
622
+ 'host setMuted a true',
623
+ 'showWindow b',
624
+ 'app b first_painted',
625
+ 'hideWindow a',
626
+ 'sendMessageString a video_closing',
627
+ 'host close a',
628
+ 'removeWindow a',
629
+ ]);
630
+ expect(timeline()).toContain('host onHole false');
631
+ expect(host.violations).toEqual([]);
632
+ return [4 /*yield*/, manager.destroy()];
633
+ case 3:
634
+ _b.sent();
635
+ return [2 /*return*/];
636
+ }
637
+ });
638
+ }); });
639
+ test('an image to video swap holds the image up until the surface is on screen', function () { return __awaiter(void 0, void 0, void 0, function () {
640
+ var _a, manager, host, timeline, complete, start, before, swap;
641
+ return __generator(this, function (_b) {
642
+ switch (_b.label) {
643
+ case 0:
644
+ _a = createSurfaceManager(['a', 'b'], { images: ['a'] }), manager = _a.manager, host = _a.host, timeline = _a.timeline, complete = _a.complete, start = _a.start;
645
+ return [4 /*yield*/, start()];
646
+ case 1:
647
+ _b.sent();
648
+ before = timeline().length;
649
+ return [4 /*yield*/, complete('a')];
650
+ case 2:
651
+ _b.sent();
652
+ swap = timeline().slice(before).filter(isSwapStep);
653
+ expect(swap).toEqual([
654
+ 'app a presentation_complete',
655
+ 'muteWindow a',
656
+ 'host show b',
657
+ 'host setMuted b false',
658
+ 'sendMessageString b video_shown',
659
+ 'app b video_hole_open',
660
+ 'setWindowBackground b transparent',
661
+ 'showWindow b',
662
+ 'hideWindow a',
663
+ 'app b first_painted',
664
+ 'removeWindow a',
665
+ ]);
666
+ expect(host.violations).toEqual([]);
667
+ return [4 /*yield*/, manager.destroy()];
668
+ case 3:
669
+ _b.sent();
670
+ return [2 /*return*/];
671
+ }
672
+ });
673
+ }); });
674
+ test.each([
675
+ ['a hung show', { hangShow: true }, {}],
676
+ ['a missing hole acknowledgement', {}, { holdHole: ['a'] }],
677
+ ])('the first presentation with %s falls back to <video> and plays', function (_, hostOpts, script) { return __awaiter(void 0, void 0, void 0, function () {
678
+ var _a, manager, host, warn, sent, hostCalls, timeline, start;
679
+ return __generator(this, function (_b) {
680
+ switch (_b.label) {
681
+ case 0:
682
+ _a = createSurfaceManager(['a'], script, hostOpts), manager = _a.manager, host = _a.host, warn = _a.warn, sent = _a.sent, hostCalls = _a.hostCalls, timeline = _a.timeline, start = _a.start;
683
+ return [4 /*yield*/, start()];
684
+ case 1:
685
+ _b.sent();
686
+ expect(sent('a', 'video_refused').map(function (c) { return c.args[1]; })).toEqual([
687
+ { reason: 'deadline' },
688
+ ]);
689
+ expect(hostCalls('close', 'a')).toHaveLength(1);
690
+ expect(timeline()).toContain('showWindow a');
691
+ expect(sent('a', 'play')).toHaveLength(1);
692
+ expect(timeline()).not.toContain('host onHole true');
693
+ expect(warn.mock.calls.filter(function (_a) {
694
+ var message = _a[0];
695
+ return /video_refused.*deadline/.test(message);
696
+ })).toHaveLength(1);
697
+ expect(host.violations).toEqual([]);
698
+ return [4 /*yield*/, manager.destroy()];
699
+ case 2:
700
+ _b.sent();
701
+ return [2 /*return*/];
702
+ }
703
+ });
704
+ }); });
705
+ test.each([
706
+ ['a hung show', 'hangShow'],
707
+ ['a missing hole acknowledgement', 'holdHole'],
708
+ ])('a swap with %s falls back to <video> and the playlist advances', function (_, fault) { return __awaiter(void 0, void 0, void 0, function () {
709
+ var script, _a, manager, host, sent, hostCalls, timeline, complete, start;
710
+ return __generator(this, function (_b) {
711
+ switch (_b.label) {
712
+ case 0:
713
+ script = fault === 'holdHole' ? { holdHole: ['b'] } : {};
714
+ _a = createSurfaceManager(['a', 'b'], script), manager = _a.manager, host = _a.host, sent = _a.sent, hostCalls = _a.hostCalls, timeline = _a.timeline, complete = _a.complete, start = _a.start;
715
+ return [4 /*yield*/, start()];
716
+ case 1:
717
+ _b.sent();
718
+ if (fault === 'hangShow')
719
+ host.hangNextShow();
720
+ return [4 /*yield*/, complete('a')];
721
+ case 2:
722
+ _b.sent();
723
+ expect(sent('b', 'video_refused').map(function (c) { return c.args[1]; })).toEqual([
724
+ { reason: 'deadline' },
725
+ ]);
726
+ expect(hostCalls('close', 'b')).toHaveLength(1);
727
+ expect(timeline()).toContain('showWindow b');
728
+ expect(sent('b', 'play')).toHaveLength(1);
729
+ expect(timeline().filter(function (line) { return line === 'host onHole true'; })).toHaveLength(1);
730
+ expect(timeline()).toContain('removeWindow a');
731
+ // The playlist advances to a again.
732
+ return [4 /*yield*/, complete('b')];
733
+ case 3:
734
+ // The playlist advances to a again.
735
+ _b.sent();
736
+ expect(sent('a', 'play').length).toBeGreaterThanOrEqual(2);
737
+ return [4 /*yield*/, manager.destroy()];
738
+ case 4:
739
+ _b.sent();
740
+ return [2 /*return*/];
741
+ }
742
+ });
743
+ }); });
744
+ test('a window shown before open_video keeps <video>', function () { return __awaiter(void 0, void 0, void 0, function () {
745
+ var _a, manager, recording, host, apps, sent, hostCalls, timeline, script;
746
+ return __generator(this, function (_b) {
747
+ switch (_b.label) {
748
+ case 0:
749
+ _a = createSurfaceManager(['a']), manager = _a.manager, recording = _a.recording, host = _a.host, apps = _a.apps, sent = _a.sent, hostCalls = _a.hostCalls, timeline = _a.timeline;
750
+ script = recording.hooks.onMessage;
751
+ recording.hooks.onMessage = function (w, type, payload) {
752
+ if (type === 'render') {
753
+ apps.presentationOf[w] = payload.presentation.id;
754
+ setTimeout(function () { return recording.receive(w, 'presentation_ready'); }, 0);
755
+ return;
756
+ }
757
+ if (script)
758
+ script(w, type, payload);
759
+ };
760
+ return [4 /*yield*/, manager.load()];
761
+ case 1:
762
+ _b.sent();
763
+ manager.show();
764
+ manager.play();
765
+ recording.receive(apps.windowOf('a'), 'open_video', {
766
+ url: 'https://cdn.test/a.mp4',
767
+ bounds: { x: 0, y: 0, width: 1920, height: 1080 },
768
+ muted: false,
769
+ });
770
+ return [4 /*yield*/, settle()];
771
+ case 2:
772
+ _b.sent();
773
+ expect(timeline()).toContain('showWindow a');
774
+ expect(hostCalls('openVideo')).toHaveLength(0);
775
+ expect(sent('a', 'video_refused').map(function (c) { return c.args[1]; })).toEqual([
776
+ { reason: 'not_offered' },
777
+ ]);
778
+ expect(sent('a', 'play')).toHaveLength(1);
779
+ expect(host.violations).toEqual([]);
780
+ return [4 /*yield*/, manager.destroy()];
781
+ case 3:
782
+ _b.sent();
783
+ return [2 /*return*/];
784
+ }
785
+ });
786
+ }); });
787
+ test('a window shown between video_opened and first_frame keeps <video>', function () { return __awaiter(void 0, void 0, void 0, function () {
788
+ var _a, manager, recording, host, apps, sent, hostCalls, timeline, script, loaded;
789
+ return __generator(this, function (_b) {
790
+ switch (_b.label) {
791
+ case 0:
792
+ _a = createSurfaceManager(['a'], { holdFirstFrame: ['a'] }), manager = _a.manager, recording = _a.recording, host = _a.host, apps = _a.apps, sent = _a.sent, hostCalls = _a.hostCalls, timeline = _a.timeline;
793
+ script = recording.hooks.onMessage;
794
+ recording.hooks.onMessage = function (w, type, payload) {
795
+ if (script)
796
+ script(w, type, payload);
797
+ if (type === 'video_opened') {
798
+ setTimeout(function () { return recording.receive(w, 'presentation_ready'); }, 0);
799
+ }
800
+ };
801
+ loaded = manager.load();
802
+ return [4 /*yield*/, settle()];
803
+ case 1:
804
+ _b.sent();
805
+ expect(sent('a', 'video_opened')).toHaveLength(1);
806
+ manager.show();
807
+ return [4 /*yield*/, loaded];
808
+ case 2:
809
+ _b.sent();
810
+ manager.play();
811
+ return [4 /*yield*/, settle()];
812
+ case 3:
813
+ _b.sent();
814
+ expect(timeline()).toContain('showWindow a');
815
+ expect(hostCalls('show')).toHaveLength(0);
816
+ expect(sent('a', 'video_refused').map(function (c) { return c.args[1]; })).toEqual([
817
+ { reason: 'not_offered' },
818
+ ]);
819
+ expect(hostCalls('close', 'a')).toHaveLength(1);
820
+ expect(sent('a', 'play')).toHaveLength(1);
821
+ expect(hostCalls('onHole')).toHaveLength(0);
822
+ expect(host.violations).toEqual([]);
823
+ // A later first frame and ask change nothing.
824
+ recording.receive(apps.windowOf('a'), 'open_video', {
825
+ url: 'https://cdn.test/a.mp4',
826
+ bounds: { x: 0, y: 0, width: 1920, height: 1080 },
827
+ muted: false,
828
+ });
829
+ return [4 /*yield*/, settle()];
830
+ case 4:
831
+ _b.sent();
832
+ expect(hostCalls('openVideo')).toHaveLength(1);
833
+ return [4 /*yield*/, manager.destroy()];
834
+ case 5:
835
+ _b.sent();
836
+ expect(host.liveIds()).toEqual([]);
837
+ return [2 /*return*/];
838
+ }
839
+ });
840
+ }); });
841
+ test('a second show() before the hole opens does nothing', function () { return __awaiter(void 0, void 0, void 0, function () {
842
+ var _a, manager, recording, apps, sent, hostCalls, timeline;
843
+ return __generator(this, function (_b) {
844
+ switch (_b.label) {
845
+ case 0:
846
+ _a = createSurfaceManager(['a'], { holdHole: ['a'] }), manager = _a.manager, recording = _a.recording, apps = _a.apps, sent = _a.sent, hostCalls = _a.hostCalls, timeline = _a.timeline;
847
+ return [4 /*yield*/, manager.load()];
848
+ case 1:
849
+ _b.sent();
850
+ manager.show();
851
+ manager.show();
852
+ manager.play();
853
+ return [4 /*yield*/, (0, testUtils_1.flush)(20)];
854
+ case 2:
855
+ _b.sent();
856
+ recording.receive(apps.windowOf('a'), 'video_hole_open');
857
+ return [4 /*yield*/, settle()];
858
+ case 3:
859
+ _b.sent();
860
+ expect(hostCalls('show')).toHaveLength(1);
861
+ expect(sent('a', 'video_shown')).toHaveLength(1);
862
+ expect(timeline().filter(function (line) { return line === 'host onHole true'; })).toHaveLength(1);
863
+ return [4 /*yield*/, manager.destroy()];
864
+ case 4:
865
+ _b.sent();
866
+ return [2 /*return*/];
867
+ }
868
+ });
869
+ }); });
870
+ test('a one-video playlist replays by seeking, and destroy closes every surface', function () { return __awaiter(void 0, void 0, void 0, function () {
871
+ var _a, manager, host, hostCalls, complete, start, plays;
872
+ return __generator(this, function (_b) {
873
+ switch (_b.label) {
874
+ case 0:
875
+ _a = createSurfaceManager([
876
+ 'a',
877
+ ]), manager = _a.manager, host = _a.host, hostCalls = _a.hostCalls, complete = _a.complete, start = _a.start;
878
+ return [4 /*yield*/, start()];
879
+ case 1:
880
+ _b.sent();
881
+ return [4 /*yield*/, complete('a')];
882
+ case 2:
883
+ _b.sent();
884
+ expect(hostCalls('seek', 'a').map(function (c) { return c.args[0]; })).toEqual([0]);
885
+ plays = host.calls.filter(function (c) { return c.method === 'seek' || c.method === 'play'; });
886
+ expect(plays.map(function (c) { return c.method; })).toEqual(['play', 'seek', 'play']);
887
+ expect(hostCalls('close')).toHaveLength(0);
888
+ return [4 /*yield*/, manager.destroy()];
889
+ case 3:
890
+ _b.sent();
891
+ expect(host.liveIds()).toEqual([]);
892
+ expect(host.calls.filter(function (c) { return c.method === 'onHole'; }).map(function (c) { return c.args[0]; })).toEqual([true, false]);
893
+ return [2 /*return*/];
894
+ }
895
+ });
896
+ }); });
897
+ test('a short clip replays once, then advances when the next is ready', function () { return __awaiter(void 0, void 0, void 0, function () {
898
+ var _a, manager, host, apps, sent, hostCalls, complete, start;
899
+ return __generator(this, function (_b) {
900
+ switch (_b.label) {
901
+ case 0:
902
+ _a = createSurfaceManager(['a', 'b'], { holdFirstFrame: ['b'] }), manager = _a.manager, host = _a.host, apps = _a.apps, sent = _a.sent, hostCalls = _a.hostCalls, complete = _a.complete, start = _a.start;
903
+ return [4 /*yield*/, start()];
904
+ case 1:
905
+ _b.sent();
906
+ return [4 /*yield*/, complete('a')];
907
+ case 2:
908
+ _b.sent();
909
+ expect(hostCalls('seek', 'a')
910
+ .map(function (c) { return c.method; })
911
+ .concat(hostCalls('play', 'a').map(function (c) { return c.method; }))).toEqual(['seek', 'play', 'play']);
912
+ expect(hostCalls('close', 'a')).toHaveLength(0);
913
+ expect(hostCalls('show', 'b')).toHaveLength(0);
914
+ apps.emitFirstFrame('b');
915
+ return [4 /*yield*/, settle()];
916
+ case 3:
917
+ _b.sent();
918
+ return [4 /*yield*/, complete('a')];
919
+ case 4:
920
+ _b.sent();
921
+ expect(hostCalls('show', 'b')).toHaveLength(1);
922
+ expect(sent('b', 'play')).toHaveLength(1);
923
+ expect(hostCalls('close', 'a')).toHaveLength(1);
924
+ expect(host.violations).toEqual([]);
925
+ return [4 /*yield*/, manager.destroy()];
926
+ case 5:
927
+ _b.sent();
928
+ return [2 /*return*/];
929
+ }
930
+ });
931
+ }); });
932
+ test('a window whose surface closed before its turn swaps as today', function () { return __awaiter(void 0, void 0, void 0, function () {
933
+ var _a, manager, recording, host, apps, hostCalls, timeline, complete, start;
934
+ return __generator(this, function (_b) {
935
+ switch (_b.label) {
936
+ case 0:
937
+ _a = createSurfaceManager(['a', 'b']), manager = _a.manager, recording = _a.recording, host = _a.host, apps = _a.apps, hostCalls = _a.hostCalls, timeline = _a.timeline, complete = _a.complete, start = _a.start;
938
+ return [4 /*yield*/, start()];
939
+ case 1:
940
+ _b.sent();
941
+ recording.receive(apps.windowOf('b'), 'close_video');
942
+ return [4 /*yield*/, settle()];
943
+ case 2:
944
+ _b.sent();
945
+ expect(hostCalls('close', 'b')).toHaveLength(1);
946
+ return [4 /*yield*/, complete('a')];
947
+ case 3:
948
+ _b.sent();
949
+ expect(hostCalls('show', 'b')).toHaveLength(0);
950
+ expect(timeline()).toContain('showWindow b');
951
+ expect(timeline()).toContain('removeWindow a');
952
+ expect(host.violations).toEqual([]);
953
+ return [4 /*yield*/, manager.destroy()];
954
+ case 4:
955
+ _b.sent();
956
+ return [2 /*return*/];
957
+ }
958
+ });
959
+ }); });
960
+ test('an error event is forwarded, then the surface closes and the frame background returns', function () { return __awaiter(void 0, void 0, void 0, function () {
961
+ var _a, manager, host, apps, sent, hostCalls, timeline, start;
962
+ return __generator(this, function (_b) {
963
+ switch (_b.label) {
964
+ case 0:
965
+ _a = createSurfaceManager(['a']), manager = _a.manager, host = _a.host, apps = _a.apps, sent = _a.sent, hostCalls = _a.hostCalls, timeline = _a.timeline, start = _a.start;
966
+ return [4 /*yield*/, start()];
967
+ case 1:
968
+ _b.sent();
969
+ host.emit(apps.surfaceOf('a'), { type: 'error', reason: 'decode' });
970
+ return [4 /*yield*/, settle()];
971
+ case 2:
972
+ _b.sent();
973
+ expect(sent('a', 'video_event').map(function (c) { return c.args[1]; })).toContainEqual({
974
+ type: 'error',
975
+ reason: 'decode',
976
+ });
977
+ expect(hostCalls('close', 'a')).toHaveLength(1);
978
+ expect(host.calls.filter(function (c) { return c.method === 'onHole'; }).map(function (c) { return c.args[0]; })).toEqual([true, false]);
979
+ expect(timeline().filter(function (line) { return line.startsWith('setWindowBackground'); })).toEqual([
980
+ 'setWindowBackground a transparent',
981
+ 'setWindowBackground a #000000',
982
+ ]);
983
+ return [4 /*yield*/, manager.destroy()];
984
+ case 3:
985
+ _b.sent();
986
+ return [2 /*return*/];
987
+ }
988
+ });
989
+ }); });
990
+ test('stalled is forwarded and leaves the surface open', function () { return __awaiter(void 0, void 0, void 0, function () {
991
+ var _a, manager, host, apps, sent, hostCalls, start;
992
+ return __generator(this, function (_b) {
993
+ switch (_b.label) {
994
+ case 0:
995
+ _a = createSurfaceManager(['a']), manager = _a.manager, host = _a.host, apps = _a.apps, sent = _a.sent, hostCalls = _a.hostCalls, start = _a.start;
996
+ return [4 /*yield*/, start()];
997
+ case 1:
998
+ _b.sent();
999
+ host.emit(apps.surfaceOf('a'), { type: 'stalled' });
1000
+ host.emit(apps.surfaceOf('a'), { type: 'playing' });
1001
+ return [4 /*yield*/, settle()];
1002
+ case 2:
1003
+ _b.sent();
1004
+ expect(sent('a', 'video_event').map(function (c) { return c.args[1].type; })).toEqual(['first_frame', 'stalled', 'playing']);
1005
+ expect(hostCalls('close', 'a')).toHaveLength(0);
1006
+ return [4 /*yield*/, manager.destroy()];
1007
+ case 3:
1008
+ _b.sent();
1009
+ return [2 /*return*/];
1010
+ }
1011
+ });
1012
+ }); });
1013
+ });
1014
+ describe('a swap survives what runs during its wait', function () {
1015
+ // A three-video playlist whose second app holds its hole closed, so the
1016
+ // swap from a to b waits; its deadline outlasts the test's steps.
1017
+ function startWaitingSwap(images) {
1018
+ if (images === void 0) { images = []; }
1019
+ return __awaiter(this, void 0, void 0, function () {
1020
+ var setup, a, b, shown, played, removed;
1021
+ return __generator(this, function (_a) {
1022
+ switch (_a.label) {
1023
+ case 0:
1024
+ setup = createSurfaceManager(['a', 'b', 'c'], { holdHole: ['b'], images: images }, undefined, { closeWindowDelayMs: 5000 });
1025
+ return [4 /*yield*/, setup.manager.load()];
1026
+ case 1:
1027
+ _a.sent();
1028
+ setup.manager.show();
1029
+ setup.manager.play();
1030
+ return [4 /*yield*/, settle()];
1031
+ case 2:
1032
+ _a.sent();
1033
+ a = setup.apps.windowOf('a');
1034
+ passTime();
1035
+ setup.recording.receive(a, 'presentation_complete');
1036
+ return [4 /*yield*/, settle()];
1037
+ case 3:
1038
+ _a.sent();
1039
+ b = setup.apps.windowOf('b');
1040
+ expect(setup.sent('b', 'video_shown')).toHaveLength(1);
1041
+ shown = function (windowId) {
1042
+ return setup.recording.calls.some(function (c) { return c.method === 'showWindow' && c.windowId === windowId; });
1043
+ };
1044
+ played = function (windowId) {
1045
+ return setup.recording.calls.filter(function (c) {
1046
+ return c.method === 'sendMessageString' &&
1047
+ c.args[0] === 'play' &&
1048
+ c.windowId === windowId;
1049
+ }).length;
1050
+ };
1051
+ removed = function (windowId) {
1052
+ return setup.recording.calls.some(function (c) { return c.method === 'removeWindow' && c.windowId === windowId; });
1053
+ };
1054
+ return [2 /*return*/, __assign(__assign({}, setup), { a: a, b: b, shown: shown, played: played, removed: removed })];
1055
+ }
1056
+ });
1057
+ });
1058
+ }
1059
+ test('a late complete from the outgoing window starts no second swap', function () { return __awaiter(void 0, void 0, void 0, function () {
1060
+ var _a, manager, recording, a, b, hostCalls, shown, played;
1061
+ return __generator(this, function (_b) {
1062
+ switch (_b.label) {
1063
+ case 0: return [4 /*yield*/, startWaitingSwap()];
1064
+ case 1:
1065
+ _a = _b.sent(), manager = _a.manager, recording = _a.recording, a = _a.a, b = _a.b, hostCalls = _a.hostCalls, shown = _a.shown, played = _a.played;
1066
+ passTime();
1067
+ recording.receive(a, 'presentation_complete');
1068
+ return [4 /*yield*/, settle()];
1069
+ case 2:
1070
+ _b.sent();
1071
+ expect(hostCalls('show')).toHaveLength(2);
1072
+ expect(manager.currentWindow).not.toBeNull();
1073
+ expect(shown(b)).toBe(false);
1074
+ recording.receive(b, 'video_hole_open');
1075
+ return [4 /*yield*/, settle()];
1076
+ case 3:
1077
+ _b.sent();
1078
+ expect(shown(b)).toBe(true);
1079
+ expect(played(b)).toBe(1);
1080
+ return [4 /*yield*/, manager.destroy()];
1081
+ case 4:
1082
+ _b.sent();
1083
+ return [2 /*return*/];
1084
+ }
1085
+ });
1086
+ }); });
1087
+ test('the next window is preloaded only once the swap has finished', function () { return __awaiter(void 0, void 0, void 0, function () {
1088
+ var _a, manager, recording, apps, b, shown, removed, c;
1089
+ return __generator(this, function (_b) {
1090
+ switch (_b.label) {
1091
+ case 0: return [4 /*yield*/, startWaitingSwap()];
1092
+ case 1:
1093
+ _a = _b.sent(), manager = _a.manager, recording = _a.recording, apps = _a.apps, b = _a.b, shown = _a.shown, removed = _a.removed;
1094
+ expect(apps.windowOf('c')).toBeUndefined();
1095
+ expect(manager.nextWindow).toBeNull();
1096
+ recording.receive(b, 'video_hole_open');
1097
+ return [4 /*yield*/, settle()];
1098
+ case 2:
1099
+ _b.sent();
1100
+ expect(shown(b)).toBe(true);
1101
+ expect(removed(apps.windowOf('a'))).toBe(true);
1102
+ c = apps.windowOf('c');
1103
+ expect(c).toBeDefined();
1104
+ expect(manager.nextWindow.isReady()).toBe(true);
1105
+ return [4 /*yield*/, manager.destroy()];
1106
+ case 3:
1107
+ _b.sent();
1108
+ return [2 /*return*/];
1109
+ }
1110
+ });
1111
+ }); });
1112
+ test('the incoming window failing while its show waits preloads the next at once, and the playlist moves on', function () { return __awaiter(void 0, void 0, void 0, function () {
1113
+ var _a, manager, recording, host, apps, a, b, shown, played, removed, c;
1114
+ return __generator(this, function (_b) {
1115
+ switch (_b.label) {
1116
+ case 0: return [4 /*yield*/, startWaitingSwap(['a'])];
1117
+ case 1:
1118
+ _a = _b.sent(), manager = _a.manager, recording = _a.recording, host = _a.host, apps = _a.apps, a = _a.a, b = _a.b, shown = _a.shown, played = _a.played, removed = _a.removed;
1119
+ // b fails mid-swap: c preloads now and b restarts while it loads.
1120
+ passTime();
1121
+ recording.fail(b, new Error('crashed'));
1122
+ return [4 /*yield*/, settle()];
1123
+ case 2:
1124
+ _b.sent();
1125
+ c = apps.windowOf('c');
1126
+ expect(c).toBeDefined();
1127
+ expect(manager.nextWindow.isReady()).toBe(true);
1128
+ // At b's next complete the manager moves on to c; b is never revealed.
1129
+ passTime();
1130
+ recording.receive(b, 'presentation_complete');
1131
+ return [4 /*yield*/, settle()];
1132
+ case 3:
1133
+ _b.sent();
1134
+ expect(removed(b)).toBe(true);
1135
+ expect(shown(b)).toBe(false);
1136
+ expect(played(b)).toBe(0);
1137
+ expect(shown(c)).toBe(true);
1138
+ expect(played(c)).toBe(1);
1139
+ expect(removed(a)).toBe(true);
1140
+ expect(host.violations).toEqual([]);
1141
+ return [4 /*yield*/, manager.destroy()];
1142
+ case 4:
1143
+ _b.sent();
1144
+ return [4 /*yield*/, settle()];
1145
+ case 5:
1146
+ _b.sent();
1147
+ expect(host.liveIds()).toEqual([]);
1148
+ return [2 /*return*/];
1149
+ }
1150
+ });
1151
+ }); });
1152
+ test('a publish during the wait preloads its next window at once, and the swap preloads none', function () { return __awaiter(void 0, void 0, void 0, function () {
1153
+ var _a, manager, recording, host, apps, b, shown, played, removed, created, createdBefore, ruleEngine, published;
1154
+ return __generator(this, function (_b) {
1155
+ switch (_b.label) {
1156
+ case 0: return [4 /*yield*/, startWaitingSwap()];
1157
+ case 1:
1158
+ _a = _b.sent(), manager = _a.manager, recording = _a.recording, host = _a.host, apps = _a.apps, b = _a.b, shown = _a.shown, played = _a.played, removed = _a.removed;
1159
+ created = function () {
1160
+ return recording.calls.filter(function (call) { return call.method === 'createWindow'; }).length;
1161
+ };
1162
+ createdBefore = created();
1163
+ ruleEngine = manager.getRuleEngine();
1164
+ published = manager.setRuleEngine(new RE.RuleEngine('main', new RE.State({
1165
+ main: {
1166
+ id: 'main',
1167
+ mode: RE.PlaylistMode.Sequential,
1168
+ items: [{ presentationId: 'b' }, { presentationId: 'a' }],
1169
+ },
1170
+ }, ruleEngine.state.presentations, {})));
1171
+ return [4 /*yield*/, settle()];
1172
+ case 2:
1173
+ _b.sent();
1174
+ expect(created()).toBe(createdBefore + 1);
1175
+ expect(removed(b)).toBe(false);
1176
+ recording.receive(b, 'video_hole_open');
1177
+ return [4 /*yield*/, settle()];
1178
+ case 3:
1179
+ _b.sent();
1180
+ return [4 /*yield*/, published];
1181
+ case 4:
1182
+ _b.sent();
1183
+ expect(shown(b)).toBe(true);
1184
+ expect(played(b)).toBe(1);
1185
+ expect(created()).toBe(createdBefore + 1);
1186
+ expect(apps.windowOf('c')).toBeUndefined();
1187
+ expect(host.violations).toEqual([]);
1188
+ return [4 /*yield*/, manager.destroy()];
1189
+ case 5:
1190
+ _b.sent();
1191
+ return [4 /*yield*/, settle()];
1192
+ case 6:
1193
+ _b.sent();
1194
+ expect(host.liveIds()).toEqual([]);
1195
+ return [2 /*return*/];
1196
+ }
1197
+ });
1198
+ }); });
1199
+ test('a preview change during the wait preloads its next window at once, and the swap preloads none', function () { return __awaiter(void 0, void 0, void 0, function () {
1200
+ var _a, manager, recording, apps, b, shown, played, created, createdBefore, ruleEngine, checked;
1201
+ return __generator(this, function (_b) {
1202
+ switch (_b.label) {
1203
+ case 0: return [4 /*yield*/, startWaitingSwap()];
1204
+ case 1:
1205
+ _a = _b.sent(), manager = _a.manager, recording = _a.recording, apps = _a.apps, b = _a.b, shown = _a.shown, played = _a.played;
1206
+ created = function () {
1207
+ return recording.calls.filter(function (call) { return call.method === 'createWindow'; }).length;
1208
+ };
1209
+ createdBefore = created();
1210
+ ruleEngine = manager.getRuleEngine();
1211
+ jest.spyOn(ruleEngine, 'previewNext').mockReturnValue({
1212
+ presentationId: 'a',
1213
+ shouldInterrupt: false,
1214
+ playlistId: 'main',
1215
+ rule: null,
1216
+ });
1217
+ checked = manager.checkPreviewAction();
1218
+ return [4 /*yield*/, settle()];
1219
+ case 2:
1220
+ _b.sent();
1221
+ expect(created()).toBe(createdBefore + 1);
1222
+ recording.receive(b, 'video_hole_open');
1223
+ return [4 /*yield*/, settle()];
1224
+ case 3:
1225
+ _b.sent();
1226
+ return [4 /*yield*/, checked];
1227
+ case 4:
1228
+ _b.sent();
1229
+ expect(shown(b)).toBe(true);
1230
+ expect(played(b)).toBe(1);
1231
+ expect(created()).toBe(createdBefore + 1);
1232
+ expect(apps.windowOf('c')).toBeUndefined();
1233
+ return [4 /*yield*/, manager.destroy()];
1234
+ case 5:
1235
+ _b.sent();
1236
+ return [2 /*return*/];
1237
+ }
1238
+ });
1239
+ }); });
1240
+ test('destroy during the wait leaves no window and no surface', function () { return __awaiter(void 0, void 0, void 0, function () {
1241
+ var _a, manager, recording, host, callsAtDestroy;
1242
+ return __generator(this, function (_b) {
1243
+ switch (_b.label) {
1244
+ case 0: return [4 /*yield*/, startWaitingSwap()];
1245
+ case 1:
1246
+ _a = _b.sent(), manager = _a.manager, recording = _a.recording, host = _a.host;
1247
+ return [4 /*yield*/, manager.destroy()];
1248
+ case 2:
1249
+ _b.sent();
1250
+ callsAtDestroy = recording.calls.length;
1251
+ return [4 /*yield*/, settle()];
1252
+ case 3:
1253
+ _b.sent();
1254
+ expect(recording.calls
1255
+ .slice(callsAtDestroy)
1256
+ .filter(function (c) { return c.method === 'createWindow'; })).toHaveLength(0);
1257
+ expect(host.liveIds()).toEqual([]);
1258
+ expect(recording.liveWindowIds()).toEqual([]);
1259
+ return [2 /*return*/];
1260
+ }
1261
+ });
1262
+ }); });
1263
+ });
1264
+ describe('swap timing, on fake timers', function () {
1265
+ beforeEach(function () {
1266
+ jest.useFakeTimers();
1267
+ });
1268
+ afterEach(function () {
1269
+ jest.useRealTimers();
1270
+ });
1271
+ // Lets promise callbacks run between fake timers.
1272
+ var microtasks = function () { return __awaiter(void 0, void 0, void 0, function () {
1273
+ var i;
1274
+ return __generator(this, function (_a) {
1275
+ switch (_a.label) {
1276
+ case 0:
1277
+ i = 0;
1278
+ _a.label = 1;
1279
+ case 1:
1280
+ if (!(i < 50)) return [3 /*break*/, 4];
1281
+ return [4 /*yield*/, Promise.resolve()];
1282
+ case 2:
1283
+ _a.sent();
1284
+ _a.label = 3;
1285
+ case 3:
1286
+ i += 1;
1287
+ return [3 /*break*/, 1];
1288
+ case 4: return [2 /*return*/];
1289
+ }
1290
+ });
1291
+ }); };
1292
+ // A surface manager driven a millisecond at a time, whose host reports
1293
+ // playing 25 ms after each play. Every call and app message is stamped
1294
+ // with the millisecond it happened in.
1295
+ function startTimed(presentationIds, script, hostOpts, showAfterRevealMs) {
1296
+ if (hostOpts === void 0) { hostOpts = {
1297
+ playingAfterMs: 25,
1298
+ }; }
1299
+ return __awaiter(this, void 0, void 0, function () {
1300
+ var setup, clock, mostLive, stamps, stamp, tick, loading, completeAt, since, settleNow;
1301
+ var _this = this;
1302
+ return __generator(this, function (_a) {
1303
+ switch (_a.label) {
1304
+ case 0:
1305
+ setup = createSurfaceManager(presentationIds, script, hostOpts, {}, showAfterRevealMs);
1306
+ clock = 0;
1307
+ mostLive = 0;
1308
+ stamps = {};
1309
+ stamp = function () {
1310
+ setup.entries().forEach(function (e) {
1311
+ if (stamps[e.seq] === undefined)
1312
+ stamps[e.seq] = clock;
1313
+ });
1314
+ mostLive = Math.max(mostLive, setup.host.liveIds().length);
1315
+ };
1316
+ tick = function (ms) { return __awaiter(_this, void 0, void 0, function () {
1317
+ var i;
1318
+ return __generator(this, function (_a) {
1319
+ switch (_a.label) {
1320
+ case 0:
1321
+ i = 0;
1322
+ _a.label = 1;
1323
+ case 1:
1324
+ if (!(i < ms)) return [3 /*break*/, 4];
1325
+ jest.advanceTimersByTime(1);
1326
+ clock += 1;
1327
+ return [4 /*yield*/, microtasks()];
1328
+ case 2:
1329
+ _a.sent();
1330
+ stamp();
1331
+ _a.label = 3;
1332
+ case 3:
1333
+ i += 1;
1334
+ return [3 /*break*/, 1];
1335
+ case 4: return [2 /*return*/];
1336
+ }
1337
+ });
1338
+ }); };
1339
+ loading = setup.manager.load();
1340
+ return [4 /*yield*/, tick(10)];
1341
+ case 1:
1342
+ _a.sent();
1343
+ return [4 /*yield*/, loading];
1344
+ case 2:
1345
+ _a.sent();
1346
+ setup.manager.show();
1347
+ setup.manager.play();
1348
+ return [4 /*yield*/, tick(200)];
1349
+ case 3:
1350
+ _a.sent();
1351
+ completeAt = function (presentationId) { return __awaiter(_this, void 0, void 0, function () {
1352
+ var entries, mark;
1353
+ return __generator(this, function (_a) {
1354
+ switch (_a.label) {
1355
+ case 0:
1356
+ passTime();
1357
+ entries = setup.entries();
1358
+ mark = {
1359
+ t0: clock,
1360
+ fromSeq: entries.length ? entries[entries.length - 1].seq : 0,
1361
+ };
1362
+ setup.recording.receive(setup.apps.windowOf(presentationId), 'presentation_complete');
1363
+ return [4 /*yield*/, microtasks()];
1364
+ case 1:
1365
+ _a.sent();
1366
+ stamp();
1367
+ return [2 /*return*/, mark];
1368
+ }
1369
+ });
1370
+ }); };
1371
+ since = function (mark) {
1372
+ stamp();
1373
+ return setup
1374
+ .entries()
1375
+ .filter(function (e) { return e.seq > mark.fromSeq; })
1376
+ .map(function (e) { return "".concat(stamps[e.seq] - mark.t0, " ").concat(e.text); });
1377
+ };
1378
+ settleNow = function () { return __awaiter(_this, void 0, void 0, function () {
1379
+ return __generator(this, function (_a) {
1380
+ switch (_a.label) {
1381
+ case 0: return [4 /*yield*/, microtasks()];
1382
+ case 1:
1383
+ _a.sent();
1384
+ stamp();
1385
+ return [2 /*return*/];
1386
+ }
1387
+ });
1388
+ }); };
1389
+ return [2 /*return*/, __assign(__assign({}, setup), { tick: tick, settleNow: settleNow, completeAt: completeAt, since: since, mostLive: function () { return mostLive; } })];
1390
+ }
1391
+ });
1392
+ });
1393
+ }
1394
+ // The swap's steps: surface calls, the frames shown, hidden, created and
1395
+ // removed, and the messages that start, show and close a surface.
1396
+ var isTimedStep = function (line) {
1397
+ return / (host (?!setBounds)|showWindow|hideWindow|removeWindow|createWindow|setWindowBackground)/.test(line) ||
1398
+ / sendMessageString \S+ (play|video_shown|video_closing|video_refused)$/.test(line) ||
1399
+ / app \S+ video_hole_open$/.test(line);
1400
+ };
1401
+ test('an app that opened its hole while hidden plays, is revealed on playing with the outgoing hidden, then is shown and unmuted', function () { return __awaiter(void 0, void 0, void 0, function () {
1402
+ var _a, manager, recording, host, apps, tick, completeAt, since, mostLive, a, b, hiddenInRevealTask, showWindow, mark;
1403
+ return __generator(this, function (_b) {
1404
+ switch (_b.label) {
1405
+ case 0: return [4 /*yield*/, startTimed(['a', 'b'], { preopenHole: ['a', 'b'] })];
1406
+ case 1:
1407
+ _a = _b.sent(), manager = _a.manager, recording = _a.recording, host = _a.host, apps = _a.apps, tick = _a.tick, completeAt = _a.completeAt, since = _a.since, mostLive = _a.mostLive;
1408
+ a = apps.windowOf('a');
1409
+ b = apps.windowOf('b');
1410
+ hiddenInRevealTask = null;
1411
+ showWindow = recording.renderer.showWindow;
1412
+ recording.renderer.showWindow = function (windowId, isFocused) {
1413
+ showWindow(windowId, isFocused);
1414
+ if (windowId === b) {
1415
+ Promise.resolve().then(function () {
1416
+ hiddenInRevealTask = recording.calls.some(function (c) { return c.method === 'hideWindow' && c.windowId === a; });
1417
+ });
1418
+ }
1419
+ };
1420
+ return [4 /*yield*/, completeAt('a')];
1421
+ case 2:
1422
+ mark = _b.sent();
1423
+ return [4 /*yield*/, tick(300)];
1424
+ case 3:
1425
+ _b.sent();
1426
+ expect(since(mark).filter(isTimedStep)).toEqual([
1427
+ '0 host setMuted a true',
1428
+ '0 host play b',
1429
+ '25 host onHole true',
1430
+ '25 setWindowBackground b transparent',
1431
+ '25 showWindow b',
1432
+ '25 hideWindow a',
1433
+ '25 sendMessageString b play',
1434
+ '105 host show b',
1435
+ '105 host setMuted b false',
1436
+ '105 sendMessageString b video_shown',
1437
+ '105 sendMessageString a video_closing',
1438
+ '105 host close a',
1439
+ '105 host onHole false',
1440
+ '105 removeWindow a',
1441
+ '105 createWindow a2',
1442
+ '106 app b video_hole_open',
1443
+ '106 host openVideo a2',
1444
+ '107 app a2 video_hole_open',
1445
+ ]);
1446
+ expect(hiddenInRevealTask).toBe(true);
1447
+ expect(mostLive()).toBeLessThanOrEqual(2);
1448
+ expect(host.violations).toEqual([]);
1449
+ return [4 /*yield*/, manager.destroy()];
1450
+ case 4:
1451
+ _b.sent();
1452
+ return [2 /*return*/];
1453
+ }
1454
+ });
1455
+ }); });
1456
+ test('an app that has not opened its hole keeps the show, hole, reveal, play order', function () { return __awaiter(void 0, void 0, void 0, function () {
1457
+ var _a, manager, host, tick, completeAt, since, mostLive, mark;
1458
+ return __generator(this, function (_b) {
1459
+ switch (_b.label) {
1460
+ case 0: return [4 /*yield*/, startTimed(['a', 'b'], {})];
1461
+ case 1:
1462
+ _a = _b.sent(), manager = _a.manager, host = _a.host, tick = _a.tick, completeAt = _a.completeAt, since = _a.since, mostLive = _a.mostLive;
1463
+ return [4 /*yield*/, completeAt('a')];
1464
+ case 2:
1465
+ mark = _b.sent();
1466
+ return [4 /*yield*/, tick(300)];
1467
+ case 3:
1468
+ _b.sent();
1469
+ expect(since(mark).filter(isTimedStep)).toEqual([
1470
+ '0 host setMuted a true',
1471
+ '0 host show b',
1472
+ '0 host setMuted b false',
1473
+ '0 sendMessageString b video_shown',
1474
+ '1 app b video_hole_open',
1475
+ '1 host onHole true',
1476
+ '1 setWindowBackground b transparent',
1477
+ '1 showWindow b',
1478
+ '1 hideWindow a',
1479
+ '1 sendMessageString b play',
1480
+ '1 host play b',
1481
+ '2 sendMessageString a video_closing',
1482
+ '2 host close a',
1483
+ '2 host onHole false',
1484
+ '2 removeWindow a',
1485
+ '2 createWindow a2',
1486
+ '3 host openVideo a2',
1487
+ ]);
1488
+ expect(mostLive()).toBeLessThanOrEqual(2);
1489
+ expect(host.violations).toEqual([]);
1490
+ return [4 /*yield*/, manager.destroy()];
1491
+ case 4:
1492
+ _b.sent();
1493
+ return [2 /*return*/];
1494
+ }
1495
+ });
1496
+ }); });
1497
+ test('a surface that never reports playing is revealed at the show deadline', function () { return __awaiter(void 0, void 0, void 0, function () {
1498
+ var _a, manager, host, warn, tick, completeAt, since, mark, steps;
1499
+ return __generator(this, function (_b) {
1500
+ switch (_b.label) {
1501
+ case 0: return [4 /*yield*/, startTimed(['a', 'b'], { preopenHole: ['b'] }, {})];
1502
+ case 1:
1503
+ _a = _b.sent(), manager = _a.manager, host = _a.host, warn = _a.warn, tick = _a.tick, completeAt = _a.completeAt, since = _a.since;
1504
+ return [4 /*yield*/, completeAt('a')];
1505
+ case 2:
1506
+ mark = _b.sent();
1507
+ return [4 /*yield*/, tick(300)];
1508
+ case 3:
1509
+ _b.sent();
1510
+ steps = since(mark).filter(isTimedStep);
1511
+ expect(steps).toContain('0 host play b');
1512
+ expect(steps).toContain("".concat(CLOSE_WINDOW_DELAY_MS, " showWindow b"));
1513
+ expect(steps).toContain("".concat(CLOSE_WINDOW_DELAY_MS + 80, " host show b"));
1514
+ expect(warn.mock.calls.filter(function (_a) {
1515
+ var message = _a[0];
1516
+ return /No 'playing'/.test(message);
1517
+ })).toHaveLength(1);
1518
+ expect(host.violations).toEqual([]);
1519
+ return [4 /*yield*/, manager.destroy()];
1520
+ case 4:
1521
+ _b.sent();
1522
+ return [2 /*return*/];
1523
+ }
1524
+ });
1525
+ }); });
1526
+ test('surfaces.showAfterRevealMs sets the show after the reveal', function () { return __awaiter(void 0, void 0, void 0, function () {
1527
+ var _a, manager, tick, completeAt, since, mark, steps;
1528
+ return __generator(this, function (_b) {
1529
+ switch (_b.label) {
1530
+ case 0: return [4 /*yield*/, startTimed(['a', 'b'], { preopenHole: ['b'] }, undefined, 30)];
1531
+ case 1:
1532
+ _a = _b.sent(), manager = _a.manager, tick = _a.tick, completeAt = _a.completeAt, since = _a.since;
1533
+ return [4 /*yield*/, completeAt('a')];
1534
+ case 2:
1535
+ mark = _b.sent();
1536
+ return [4 /*yield*/, tick(300)];
1537
+ case 3:
1538
+ _b.sent();
1539
+ steps = since(mark).filter(isTimedStep);
1540
+ expect(steps).toContain('25 showWindow b');
1541
+ expect(steps).toContain('55 host show b');
1542
+ expect(steps).toContain('55 host setMuted b false');
1543
+ return [4 /*yield*/, manager.destroy()];
1544
+ case 4:
1545
+ _b.sent();
1546
+ return [2 /*return*/];
1547
+ }
1548
+ });
1549
+ }); });
1550
+ test('over a playlist of swaps, two surfaces never sound at once and never more than two are live', function () { return __awaiter(void 0, void 0, void 0, function () {
1551
+ var _a, manager, host, tick, completeAt, hostCalls, mostLive, _i, _b, id;
1552
+ return __generator(this, function (_c) {
1553
+ switch (_c.label) {
1554
+ case 0: return [4 /*yield*/, startTimed(['a', 'b', 'c'], { preopenHole: ['a', 'b', 'c'] })];
1555
+ case 1:
1556
+ _a = _c.sent(), manager = _a.manager, host = _a.host, tick = _a.tick, completeAt = _a.completeAt, hostCalls = _a.hostCalls, mostLive = _a.mostLive;
1557
+ _i = 0, _b = ['a', 'b', 'c', 'a'];
1558
+ _c.label = 2;
1559
+ case 2:
1560
+ if (!(_i < _b.length)) return [3 /*break*/, 6];
1561
+ id = _b[_i];
1562
+ return [4 /*yield*/, completeAt(id)];
1563
+ case 3:
1564
+ _c.sent();
1565
+ return [4 /*yield*/, tick(1000)];
1566
+ case 4:
1567
+ _c.sent();
1568
+ _c.label = 5;
1569
+ case 5:
1570
+ _i++;
1571
+ return [3 /*break*/, 2];
1572
+ case 6:
1573
+ expect(hostCalls('show')).toHaveLength(5);
1574
+ expect(mostLive()).toBe(2);
1575
+ expect(host.violations).toEqual([]);
1576
+ return [4 /*yield*/, manager.destroy()];
1577
+ case 7:
1578
+ _c.sent();
1579
+ expect(host.liveIds()).toEqual([]);
1580
+ return [2 /*return*/];
1581
+ }
1582
+ });
1583
+ }); });
1584
+ test('a publish while the next preload waits for the swap preloads at once, and once', function () { return __awaiter(void 0, void 0, void 0, function () {
1585
+ var _a, manager, apps, tick, settleNow, completeAt, since, host, sent, mark, ruleEngine, steps;
1586
+ return __generator(this, function (_b) {
1587
+ switch (_b.label) {
1588
+ case 0: return [4 /*yield*/, startTimed(['a', 'b', 'c'], { preopenHole: ['a', 'b', 'c'] })];
1589
+ case 1:
1590
+ _a = _b.sent(), manager = _a.manager, apps = _a.apps, tick = _a.tick, settleNow = _a.settleNow, completeAt = _a.completeAt, since = _a.since, host = _a.host, sent = _a.sent;
1591
+ return [4 /*yield*/, completeAt('a')];
1592
+ case 2:
1593
+ mark = _b.sent();
1594
+ return [4 /*yield*/, tick(10)];
1595
+ case 3:
1596
+ _b.sent();
1597
+ ruleEngine = manager.getRuleEngine();
1598
+ manager.setRuleEngine(new RE.RuleEngine('main', new RE.State({
1599
+ main: {
1600
+ id: 'main',
1601
+ mode: RE.PlaylistMode.Sequential,
1602
+ items: [{ presentationId: 'a' }, { presentationId: 'c' }],
1603
+ },
1604
+ }, ruleEngine.state.presentations, {})));
1605
+ return [4 /*yield*/, settleNow()];
1606
+ case 4:
1607
+ _b.sent();
1608
+ return [4 /*yield*/, tick(300)];
1609
+ case 5:
1610
+ _b.sent();
1611
+ steps = since(mark).filter(isTimedStep);
1612
+ expect(steps.filter(function (line) { return / createWindow /.test(line); })).toEqual([
1613
+ '10 createWindow a2',
1614
+ ]);
1615
+ expect(steps).toContain('105 host close a');
1616
+ expect(steps).toContain('105 host openVideo a2');
1617
+ expect(sent('b', 'video_shown')).toHaveLength(1);
1618
+ expect(apps.windowOf('a')).not.toBe(undefined);
1619
+ expect(host.violations).toEqual([]);
1620
+ return [4 /*yield*/, manager.destroy()];
1621
+ case 6:
1622
+ _b.sent();
1623
+ return [2 /*return*/];
1624
+ }
1625
+ });
1626
+ }); });
1627
+ test('a show that hangs after the reveal falls back to <video> in the revealed frame', function () { return __awaiter(void 0, void 0, void 0, function () {
1628
+ var _a, manager, host, tick, completeAt, since, sent, hostCalls, mark, deadlineAt, steps;
1629
+ return __generator(this, function (_b) {
1630
+ switch (_b.label) {
1631
+ case 0: return [4 /*yield*/, startTimed(['a', 'b'], { preopenHole: ['b'] })];
1632
+ case 1:
1633
+ _a = _b.sent(), manager = _a.manager, host = _a.host, tick = _a.tick, completeAt = _a.completeAt, since = _a.since, sent = _a.sent, hostCalls = _a.hostCalls;
1634
+ host.hangNextShow();
1635
+ return [4 /*yield*/, completeAt('a')];
1636
+ case 2:
1637
+ mark = _b.sent();
1638
+ return [4 /*yield*/, tick(300)];
1639
+ case 3:
1640
+ _b.sent();
1641
+ expect(sent('b', 'video_refused').map(function (c) { return c.args[1]; })).toEqual([
1642
+ { reason: 'deadline' },
1643
+ ]);
1644
+ deadlineAt = 105 + CLOSE_WINDOW_DELAY_MS;
1645
+ steps = since(mark).filter(isTimedStep);
1646
+ expect(steps).toContain('105 host show b');
1647
+ expect(steps).toContain("".concat(deadlineAt, " host close b"));
1648
+ expect(steps).toContain("".concat(deadlineAt, " setWindowBackground b #000000"));
1649
+ expect(steps.filter(function (line) { return / showWindow b$/.test(line); })).toEqual([
1650
+ '25 showWindow b',
1651
+ ]);
1652
+ expect(sent('b', 'play')).toHaveLength(1);
1653
+ expect(sent('b', 'video_shown')).toHaveLength(0);
1654
+ expect(hostCalls('setMuted', 'b')).toHaveLength(0);
1655
+ expect(steps).toContain("".concat(deadlineAt, " removeWindow a"));
1656
+ expect(host.violations).toEqual([]);
1657
+ return [4 /*yield*/, manager.destroy()];
1658
+ case 4:
1659
+ _b.sent();
1660
+ return [2 /*return*/];
1661
+ }
1662
+ });
1663
+ }); });
1664
+ test('a swap of two still images in a surface manager preloads in the swap and removes the outgoing at its paint', function () { return __awaiter(void 0, void 0, void 0, function () {
1665
+ var _a, manager, tick, completeAt, since, mark;
1666
+ return __generator(this, function (_b) {
1667
+ switch (_b.label) {
1668
+ case 0: return [4 /*yield*/, startTimed(['a', 'b'], {
1669
+ images: ['a', 'b'],
1670
+ })];
1671
+ case 1:
1672
+ _a = _b.sent(), manager = _a.manager, tick = _a.tick, completeAt = _a.completeAt, since = _a.since;
1673
+ return [4 /*yield*/, completeAt('a')];
1674
+ case 2:
1675
+ mark = _b.sent();
1676
+ return [4 /*yield*/, tick(100)];
1677
+ case 3:
1678
+ _b.sent();
1679
+ expect(since(mark).filter(isTimedStep)).toEqual([
1680
+ '0 showWindow b',
1681
+ '0 sendMessageString b play',
1682
+ '0 createWindow a2',
1683
+ '1 removeWindow a',
1684
+ ]);
1685
+ return [4 /*yield*/, manager.destroy()];
1686
+ case 4:
1687
+ _b.sent();
1688
+ return [2 /*return*/];
1689
+ }
1690
+ });
1691
+ }); });
1692
+ test('a manager with no surface host preloads in the swap and removes the outgoing at its paint', function () { return __awaiter(void 0, void 0, void 0, function () {
1693
+ var _a, manager, recording, tick, loading, before, inSwapTask;
1694
+ return __generator(this, function (_b) {
1695
+ switch (_b.label) {
1696
+ case 0:
1697
+ _a = createManager({
1698
+ presentationIds: ['a', 'b'],
1699
+ }), manager = _a.manager, recording = _a.recording;
1700
+ scriptImageApp(recording);
1701
+ tick = function (ms) { return __awaiter(void 0, void 0, void 0, function () {
1702
+ var i;
1703
+ return __generator(this, function (_a) {
1704
+ switch (_a.label) {
1705
+ case 0:
1706
+ i = 0;
1707
+ _a.label = 1;
1708
+ case 1:
1709
+ if (!(i < ms)) return [3 /*break*/, 4];
1710
+ jest.advanceTimersByTime(1);
1711
+ return [4 /*yield*/, microtasks()];
1712
+ case 2:
1713
+ _a.sent();
1714
+ _a.label = 3;
1715
+ case 3:
1716
+ i += 1;
1717
+ return [3 /*break*/, 1];
1718
+ case 4: return [2 /*return*/];
1719
+ }
1720
+ });
1721
+ }); };
1722
+ loading = manager.load();
1723
+ return [4 /*yield*/, tick(10)];
1724
+ case 1:
1725
+ _b.sent();
1726
+ return [4 /*yield*/, loading];
1727
+ case 2:
1728
+ _b.sent();
1729
+ manager.show();
1730
+ manager.play();
1731
+ return [4 /*yield*/, tick(10)];
1732
+ case 3:
1733
+ _b.sent();
1734
+ before = recording.calls.length;
1735
+ passTime();
1736
+ recording.receive(lastShown(recording), 'presentation_complete');
1737
+ return [4 /*yield*/, microtasks()];
1738
+ case 4:
1739
+ _b.sent();
1740
+ inSwapTask = describeCalls(recording).slice(before);
1741
+ return [4 /*yield*/, tick(5)];
1742
+ case 5:
1743
+ _b.sent();
1744
+ // w1 is the incoming window, preloaded before w2 at load.
1745
+ expect(inSwapTask).toEqual([
1746
+ 'showWindow w1',
1747
+ 'unMuteWindow w1',
1748
+ 'muteWindow w2',
1749
+ 'sendMessageString w1 play',
1750
+ 'createWindow w3',
1751
+ 'muteWindow w3',
1752
+ 'loadWindow w3',
1753
+ 'sendMessageString w3 render',
1754
+ ]);
1755
+ expect(describeCalls(recording).slice(before)).toContain('removeWindow w2');
1756
+ return [4 /*yield*/, manager.destroy()];
1757
+ case 6:
1758
+ _b.sent();
1759
+ return [2 /*return*/];
1760
+ }
1761
+ });
1762
+ }); });
1763
+ });