@raydiant/playlist-window-manager 0.29.2 → 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.
- package/build/IFrameRenderer.d.ts +1 -0
- package/build/IFrameRenderer.js +14 -0
- package/build/IFrameRenderer.test.d.ts +1 -0
- package/build/IFrameRenderer.test.js +70 -0
- package/build/PlaylistWindowManager.d.ts +7 -0
- package/build/PlaylistWindowManager.js +150 -39
- package/build/PlaylistWindowManager.test.d.ts +1 -0
- package/build/PlaylistWindowManager.test.js +1763 -0
- package/build/PresentationWindow.d.ts +31 -1
- package/build/PresentationWindow.js +538 -30
- package/build/PresentationWindow.test.js +323 -0
- package/build/SurfaceBroker.d.ts +30 -0
- package/build/SurfaceBroker.js +311 -0
- package/build/SurfaceBroker.test.d.ts +1 -0
- package/build/SurfaceBroker.test.js +432 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +1 -0
- package/build/surfaces.d.ts +52 -0
- package/build/surfaces.js +15 -0
- package/build/testUtils.d.ts +56 -0
- package/build/testUtils.js +327 -5
- package/build/types.d.ts +1 -0
- package/package.json +2 -1
- package/src/IFrameRenderer.test.ts +88 -0
- package/src/IFrameRenderer.ts +21 -0
- package/src/PlaylistWindowManager.test.ts +1466 -0
- package/src/PlaylistWindowManager.ts +134 -7
- package/src/PresentationWindow.test.ts +247 -0
- package/src/PresentationWindow.ts +644 -3
- package/src/SurfaceBroker.test.ts +278 -0
- package/src/SurfaceBroker.ts +281 -0
- package/src/__snapshots__/PlaylistWindowManager.test.ts.snap +26 -0
- package/src/index.ts +1 -0
- package/src/surfaces.ts +82 -0
- package/src/testUtils.ts +335 -1
- package/src/types.ts +5 -0
|
@@ -19,6 +19,11 @@ import PlaylistWindowManagerState from './PlaylistWindowManagerState';
|
|
|
19
19
|
import WindowUnresponsiveError from './WindowUnresponsiveError';
|
|
20
20
|
import WindowTimeoutError from './WindowTimeoutError';
|
|
21
21
|
import WindowLoadError from './WindowLoadError';
|
|
22
|
+
import SurfaceBroker, {
|
|
23
|
+
DEFAULT_SHOW_AFTER_REVEAL_MS,
|
|
24
|
+
DEFAULT_SURFACE_SWAP_DELAY_MS,
|
|
25
|
+
} from './SurfaceBroker';
|
|
26
|
+
import { SurfaceHost } from './surfaces';
|
|
22
27
|
|
|
23
28
|
export interface PlaylistWindowManagerOptions {
|
|
24
29
|
// The rule engine
|
|
@@ -83,6 +88,12 @@ export interface PlaylistWindowManagerOptions {
|
|
|
83
88
|
|
|
84
89
|
// Called when a window needs to check if the device is online.
|
|
85
90
|
getOnlineStatus?: () => boolean;
|
|
91
|
+
|
|
92
|
+
// The host's video surface, bound by the player, when the host can draw
|
|
93
|
+
// one. Absent, nothing changes: apps are offered no surface and keep
|
|
94
|
+
// <video>. A zone's manager is never given one. Its showAfterRevealMs is
|
|
95
|
+
// the device's calibration for a swap's reveal; see SurfaceHost.
|
|
96
|
+
surfaces?: SurfaceHost;
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
// Wait at least 3 seconds before restarting or moving onto the next window
|
|
@@ -118,6 +129,10 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
118
129
|
private isFocused: boolean = false;
|
|
119
130
|
|
|
120
131
|
private workerState: WorkerState = {};
|
|
132
|
+
private surfaceBroker: SurfaceBroker | null = null;
|
|
133
|
+
// The surface swap the next window's preload waits to finish. A newer
|
|
134
|
+
// preload or the next window's removal clears it, cancelling that preload.
|
|
135
|
+
private pendingPreload: Promise<void> | null = null;
|
|
121
136
|
|
|
122
137
|
constructor(private options: PlaylistWindowManagerOptions) {
|
|
123
138
|
super();
|
|
@@ -127,6 +142,9 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
127
142
|
this.bounds = options.bounds;
|
|
128
143
|
this.themeOverride = options.themeOverride;
|
|
129
144
|
this.forceMute = options.forceMute || false;
|
|
145
|
+
if (options.surfaces) {
|
|
146
|
+
this.surfaceBroker = new SurfaceBroker(options.surfaces, options.logger);
|
|
147
|
+
}
|
|
130
148
|
}
|
|
131
149
|
|
|
132
150
|
private currentPresentationId(): string | null {
|
|
@@ -357,6 +375,10 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
357
375
|
clearTimeout(this.currentPresentationCompleteTimeout);
|
|
358
376
|
}
|
|
359
377
|
await Promise.all([this.removeCurrentWindow(), this.removeNextWindow()]);
|
|
378
|
+
if (this.surfaceBroker) {
|
|
379
|
+
// Also closes a surface whose window is still leaving a swap.
|
|
380
|
+
await this.surfaceBroker.closeAll();
|
|
381
|
+
}
|
|
360
382
|
}
|
|
361
383
|
|
|
362
384
|
public updateBounds(bounds: WindowBounds) {
|
|
@@ -571,9 +593,29 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
571
593
|
workerState: this.workerState,
|
|
572
594
|
getTerminal: this.options.getTerminal,
|
|
573
595
|
getOnlineStatus: this.options.getOnlineStatus,
|
|
596
|
+
...(this.surfaceBroker
|
|
597
|
+
? {
|
|
598
|
+
surfaceBroker: this.surfaceBroker,
|
|
599
|
+
surfaceShowDeadlineMs: this.surfaceSwapDelayMs(),
|
|
600
|
+
}
|
|
601
|
+
: {}),
|
|
574
602
|
});
|
|
575
603
|
}
|
|
576
604
|
|
|
605
|
+
private showAfterRevealMs() {
|
|
606
|
+
const { surfaces } = this.options;
|
|
607
|
+
return surfaces && surfaces.showAfterRevealMs !== undefined
|
|
608
|
+
? surfaces.showAfterRevealMs
|
|
609
|
+
: DEFAULT_SHOW_AFTER_REVEAL_MS;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private surfaceSwapDelayMs() {
|
|
613
|
+
const { closeWindowDelayMs } = this.options;
|
|
614
|
+
return closeWindowDelayMs !== undefined
|
|
615
|
+
? closeWindowDelayMs
|
|
616
|
+
: DEFAULT_SURFACE_SWAP_DELAY_MS;
|
|
617
|
+
}
|
|
618
|
+
|
|
577
619
|
private async loadCurrentWindow() {
|
|
578
620
|
const { logger } = this.options;
|
|
579
621
|
|
|
@@ -620,6 +662,10 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
620
662
|
private async preloadNextWindow() {
|
|
621
663
|
const { logger } = this.options;
|
|
622
664
|
|
|
665
|
+
// A publish, a preview change or a complete preloads now, in place of a
|
|
666
|
+
// preload still waiting for its surface swap.
|
|
667
|
+
this.pendingPreload = null;
|
|
668
|
+
|
|
623
669
|
if (!this.nextPresentation) {
|
|
624
670
|
logger.warn(
|
|
625
671
|
`Attempted to preload next window but nextPresentation not set`
|
|
@@ -699,6 +745,7 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
699
745
|
}
|
|
700
746
|
|
|
701
747
|
private async removeNextWindow() {
|
|
748
|
+
this.pendingPreload = null;
|
|
702
749
|
if (this.nextWindow) {
|
|
703
750
|
await this.nextWindow.remove();
|
|
704
751
|
}
|
|
@@ -742,17 +789,29 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
742
789
|
// Clear the pending interrupt whenever we swap the current window for the next one.
|
|
743
790
|
this.hasPendingInterrupt = false;
|
|
744
791
|
|
|
745
|
-
//
|
|
746
|
-
//
|
|
747
|
-
|
|
748
|
-
|
|
792
|
+
// Only a manager given a surface host, swapping a window that holds a
|
|
793
|
+
// surface, takes the surface path; every other swap runs today's code.
|
|
794
|
+
const isSurfaceSwap =
|
|
795
|
+
!!this.surfaceBroker &&
|
|
796
|
+
((!!this.currentWindow && this.currentWindow.holdsSurface()) ||
|
|
797
|
+
(!!this.nextWindow && this.nextWindow.holdsSurface()));
|
|
798
|
+
|
|
799
|
+
let closeWindowPromise;
|
|
800
|
+
if (isSurfaceSwap) {
|
|
801
|
+
closeWindowPromise = this.swapWithSurfaces(
|
|
802
|
+
this.currentWindow,
|
|
803
|
+
this.nextWindow
|
|
804
|
+
);
|
|
805
|
+
} else if (this.nextWindow) {
|
|
806
|
+
// Next window is ready, show the next one, hide the current window, and
|
|
807
|
+
// then remove the current window. Show the next window before hiding the
|
|
808
|
+
// current to avoid flashes when rendering with BrowserWindows on device.
|
|
749
809
|
this.nextWindow.show(this.isFocused);
|
|
750
810
|
}
|
|
751
811
|
|
|
752
812
|
// If current window isn't defined we don't need to hide it but we still
|
|
753
813
|
// want to make the next window the new current window.
|
|
754
|
-
|
|
755
|
-
if (this.currentWindow) {
|
|
814
|
+
if (!isSurfaceSwap && this.currentWindow) {
|
|
756
815
|
if (this.options.closeWindowDelayMs !== undefined) {
|
|
757
816
|
// Keep the outgoing window on screen until the incoming one reports
|
|
758
817
|
// its first painted frame. A newly shown window still needs a few
|
|
@@ -814,7 +873,16 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
814
873
|
this.playCurrentWindow();
|
|
815
874
|
|
|
816
875
|
// Preload the next presentation if there is one.
|
|
817
|
-
|
|
876
|
+
let preloadAfterSwap: Promise<void> | null = null;
|
|
877
|
+
if (shouldPreload && isSurfaceSwap) {
|
|
878
|
+
// Once the swap has finished, so the next surface's decoder does not
|
|
879
|
+
// start during it: on the Stick a preload started in the swap held the
|
|
880
|
+
// outgoing clip's last frame for about 70 ms. The next window is the
|
|
881
|
+
// current one now.
|
|
882
|
+
this.nextWindow = null;
|
|
883
|
+
preloadAfterSwap = closeWindowPromise as Promise<void>;
|
|
884
|
+
this.pendingPreload = preloadAfterSwap;
|
|
885
|
+
} else if (shouldPreload) {
|
|
818
886
|
this.preloadNextWindow();
|
|
819
887
|
} else {
|
|
820
888
|
// Set next window to null to reset preload state. We don't want to
|
|
@@ -826,6 +894,58 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
826
894
|
if (closeWindowPromise) {
|
|
827
895
|
await closeWindowPromise;
|
|
828
896
|
}
|
|
897
|
+
|
|
898
|
+
if (preloadAfterSwap && this.pendingPreload === preloadAfterSwap) {
|
|
899
|
+
this.preloadNextWindow();
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// The swap when either window holds a video surface. The outgoing surface
|
|
904
|
+
// is muted and marked leaving here, in the swap's own task; the rest runs
|
|
905
|
+
// on the two windows held here, so a publish, a preview check or a preload
|
|
906
|
+
// error during its waits acts on the new next window, never these two.
|
|
907
|
+
private swapWithSurfaces(
|
|
908
|
+
outgoing: PresentationWindow | null,
|
|
909
|
+
incoming: PresentationWindow | null
|
|
910
|
+
): Promise<void> {
|
|
911
|
+
const delayMs = this.surfaceSwapDelayMs();
|
|
912
|
+
|
|
913
|
+
if (outgoing) {
|
|
914
|
+
// A late complete or error from the outgoing window must not end the
|
|
915
|
+
// incoming presentation, and two surfaces never sound at once.
|
|
916
|
+
outgoing.detachCallbacks();
|
|
917
|
+
outgoing.muteSurfaceOrFrame();
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
return (async () => {
|
|
921
|
+
let isOutgoingHidden = false;
|
|
922
|
+
if (incoming) {
|
|
923
|
+
// The new is shown before the old is hidden: an incoming surface's
|
|
924
|
+
// frame is revealed over its open hole and the outgoing frame hidden
|
|
925
|
+
// in that same task, so neither the outgoing paint-over nor its theme
|
|
926
|
+
// shows through the incoming hole.
|
|
927
|
+
await incoming.show(
|
|
928
|
+
this.isFocused,
|
|
929
|
+
() => {
|
|
930
|
+
if (outgoing) {
|
|
931
|
+
outgoing.hide();
|
|
932
|
+
isOutgoingHidden = true;
|
|
933
|
+
}
|
|
934
|
+
},
|
|
935
|
+
this.showAfterRevealMs()
|
|
936
|
+
);
|
|
937
|
+
// A removed incoming window never paints.
|
|
938
|
+
if (outgoing && !incoming.wasRemoved()) {
|
|
939
|
+
await incoming.waitForFirstPaint(delayMs);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (outgoing) {
|
|
943
|
+
if (!isOutgoingHidden) {
|
|
944
|
+
outgoing.hide();
|
|
945
|
+
}
|
|
946
|
+
await outgoing.remove();
|
|
947
|
+
}
|
|
948
|
+
})();
|
|
829
949
|
}
|
|
830
950
|
|
|
831
951
|
private evalRuleEngine(warnIfPreloadMismatch = true): EvalRuleEngineResult {
|
|
@@ -939,6 +1059,13 @@ export default class PlaylistWindowManager extends EventEmitter {
|
|
|
939
1059
|
// Clear current window error flag.
|
|
940
1060
|
this.currentWindowError = null;
|
|
941
1061
|
|
|
1062
|
+
// A complete while the next window's preload waits for its surface swap
|
|
1063
|
+
// preloads it now, and the current window restarts while it loads, as
|
|
1064
|
+
// for any next window not yet ready.
|
|
1065
|
+
if (this.pendingPreload && this.nextPresentation) {
|
|
1066
|
+
this.preloadNextWindow();
|
|
1067
|
+
}
|
|
1068
|
+
|
|
942
1069
|
// If not locked or paused, check if we can move to the next presentation.
|
|
943
1070
|
if (
|
|
944
1071
|
!this.isLocked() &&
|
|
@@ -5,7 +5,12 @@ import {
|
|
|
5
5
|
createRenderer,
|
|
6
6
|
createPlaylistWindowManagerState,
|
|
7
7
|
createRuleEngineState,
|
|
8
|
+
createFakeSurfaceHost,
|
|
9
|
+
createLogger,
|
|
8
10
|
} from './testUtils';
|
|
11
|
+
import * as re from '@raydiant/playlist-rule-engine';
|
|
12
|
+
import SurfaceBroker from './SurfaceBroker';
|
|
13
|
+
import { ApplicationVersion } from './types';
|
|
9
14
|
|
|
10
15
|
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
11
16
|
|
|
@@ -578,3 +583,245 @@ describe('detachCallbacks', () => {
|
|
|
578
583
|
expect(onComplete).not.toHaveBeenCalled();
|
|
579
584
|
});
|
|
580
585
|
});
|
|
586
|
+
|
|
587
|
+
describe('video surfaces', () => {
|
|
588
|
+
const CONTENT_URL = 'https://cdn.test/clip.mp4';
|
|
589
|
+
|
|
590
|
+
function setupVideoWindow(
|
|
591
|
+
opts: {
|
|
592
|
+
withHost?: boolean;
|
|
593
|
+
applicationVariables?: Record<string, unknown>;
|
|
594
|
+
applicationVersion?: Partial<ApplicationVersion>;
|
|
595
|
+
} = {}
|
|
596
|
+
) {
|
|
597
|
+
const testRenderer = createRenderer();
|
|
598
|
+
const host = createFakeSurfaceHost();
|
|
599
|
+
const warn = jest.fn();
|
|
600
|
+
const logger = createLogger({ warn });
|
|
601
|
+
const window = createPresentationWindow({
|
|
602
|
+
windowId: 'video-window',
|
|
603
|
+
presentation: createPresentation({
|
|
604
|
+
id: 'p1',
|
|
605
|
+
applicationVariables: opts.applicationVariables ?? {
|
|
606
|
+
content: { url: CONTENT_URL, filename: 'clip.mp4' },
|
|
607
|
+
},
|
|
608
|
+
}),
|
|
609
|
+
applicationVersion: createApplicationVersion(opts.applicationVersion),
|
|
610
|
+
renderer: testRenderer,
|
|
611
|
+
logger,
|
|
612
|
+
surfaceBroker:
|
|
613
|
+
opts.withHost === false
|
|
614
|
+
? undefined
|
|
615
|
+
: new SurfaceBroker(host.surfaces, logger),
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const messages = () =>
|
|
619
|
+
testRenderer.sendMessageString.mock.calls.map(([, message]) =>
|
|
620
|
+
JSON.parse(message)
|
|
621
|
+
);
|
|
622
|
+
const refusals = () =>
|
|
623
|
+
messages()
|
|
624
|
+
.filter((m) => m.type === 'video_refused')
|
|
625
|
+
.map((m) => m.payload.reason);
|
|
626
|
+
const refusalsLogged = () =>
|
|
627
|
+
warn.mock.calls.filter(([message]) => /video_refused/.test(message))
|
|
628
|
+
.length;
|
|
629
|
+
const openVideo = (url: string) =>
|
|
630
|
+
testRenderer.runReceiveMessageCallback('open_video', {
|
|
631
|
+
url,
|
|
632
|
+
bounds: { x: 0.4, y: 10.6, width: 1919.6, height: 1080 },
|
|
633
|
+
muted: false,
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
return {
|
|
637
|
+
testRenderer,
|
|
638
|
+
host,
|
|
639
|
+
window,
|
|
640
|
+
messages,
|
|
641
|
+
refusals,
|
|
642
|
+
refusalsLogged,
|
|
643
|
+
openVideo,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
test('open_video reaches the host, and render carries nativeVideo, only with a host', async () => {
|
|
648
|
+
const offered = setupVideoWindow();
|
|
649
|
+
offered.window.load();
|
|
650
|
+
await delay(0);
|
|
651
|
+
offered.testRenderer.runReceiveMessageCallback('loaded', undefined);
|
|
652
|
+
expect(
|
|
653
|
+
offered.messages().find((m) => m.type === 'render').payload.nativeVideo
|
|
654
|
+
).toBe(true);
|
|
655
|
+
|
|
656
|
+
offered.openVideo(CONTENT_URL);
|
|
657
|
+
await delay(0);
|
|
658
|
+
expect(
|
|
659
|
+
offered.host.calls.filter((c) => c.method === 'openVideo')[0].args[0]
|
|
660
|
+
).toEqual({
|
|
661
|
+
url: CONTENT_URL,
|
|
662
|
+
bounds: { x: 0, y: 11, width: 1920, height: 1080 },
|
|
663
|
+
muted: true,
|
|
664
|
+
label: 'p1',
|
|
665
|
+
});
|
|
666
|
+
expect(offered.messages().map((m) => m.type)).toContain('video_opened');
|
|
667
|
+
await offered.window.remove();
|
|
668
|
+
|
|
669
|
+
const unoffered = setupVideoWindow({ withHost: false });
|
|
670
|
+
unoffered.window.load();
|
|
671
|
+
await delay(0);
|
|
672
|
+
unoffered.testRenderer.runReceiveMessageCallback('loaded', undefined);
|
|
673
|
+
expect(
|
|
674
|
+
unoffered.messages().find((m) => m.type === 'render').payload
|
|
675
|
+
).not.toHaveProperty('nativeVideo');
|
|
676
|
+
|
|
677
|
+
unoffered.openVideo(CONTENT_URL);
|
|
678
|
+
expect(unoffered.refusals()).toEqual(['not_offered']);
|
|
679
|
+
expect(unoffered.refusalsLogged()).toBe(1);
|
|
680
|
+
await unoffered.window.remove();
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
test('a rendition beside the presentation url opens a surface', async () => {
|
|
684
|
+
const { window, host, openVideo, refusals } = setupVideoWindow();
|
|
685
|
+
window.load();
|
|
686
|
+
await delay(0);
|
|
687
|
+
|
|
688
|
+
openVideo(`${CONTENT_URL}_720p`);
|
|
689
|
+
await delay(0);
|
|
690
|
+
|
|
691
|
+
expect(host.calls.filter((c) => c.method === 'openVideo')).toHaveLength(1);
|
|
692
|
+
expect(refusals()).toEqual([]);
|
|
693
|
+
await window.remove();
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
test.each([
|
|
697
|
+
['a url the presentation does not hold', 'https://cdn.test/other.mp4'],
|
|
698
|
+
['a url beside it with another suffix', `${CONTENT_URL}_1080p`],
|
|
699
|
+
])('%s is refused not_offered', async (_, url) => {
|
|
700
|
+
const { window, host, openVideo, refusals, refusalsLogged } =
|
|
701
|
+
setupVideoWindow();
|
|
702
|
+
window.load();
|
|
703
|
+
await delay(0);
|
|
704
|
+
|
|
705
|
+
openVideo(url);
|
|
706
|
+
await delay(0);
|
|
707
|
+
|
|
708
|
+
expect(refusals()).toEqual(['not_offered']);
|
|
709
|
+
expect(refusalsLogged()).toBe(1);
|
|
710
|
+
expect(host.calls).toEqual([]);
|
|
711
|
+
await window.remove();
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
test('a url that is not http(s) is refused not_offered', async () => {
|
|
715
|
+
const { window, host, openVideo, refusals } = setupVideoWindow({
|
|
716
|
+
applicationVariables: { content: { url: 'blob:https://cdn.test/1' } },
|
|
717
|
+
});
|
|
718
|
+
window.load();
|
|
719
|
+
await delay(0);
|
|
720
|
+
|
|
721
|
+
openVideo('blob:https://cdn.test/1');
|
|
722
|
+
await delay(0);
|
|
723
|
+
|
|
724
|
+
expect(refusals()).toEqual(['not_offered']);
|
|
725
|
+
expect(host.calls).toEqual([]);
|
|
726
|
+
await window.remove();
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
test('an embedded app is refused not_offered', async () => {
|
|
730
|
+
const { window, host, testRenderer, refusals, refusalsLogged } =
|
|
731
|
+
setupVideoWindow({
|
|
732
|
+
applicationVersion: { embeddedUrlFormat: 'https://embedded.test/' },
|
|
733
|
+
});
|
|
734
|
+
await window.load();
|
|
735
|
+
|
|
736
|
+
testRenderer.runReceiveMessageCallback('open_video', {
|
|
737
|
+
url: CONTENT_URL,
|
|
738
|
+
bounds: { x: 0, y: 0, width: 1920, height: 1080 },
|
|
739
|
+
});
|
|
740
|
+
await delay(0);
|
|
741
|
+
|
|
742
|
+
expect(refusals()).toEqual(['not_offered']);
|
|
743
|
+
expect(refusalsLogged()).toBe(1);
|
|
744
|
+
expect(host.calls).toEqual([]);
|
|
745
|
+
await window.remove();
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
test("a zone's windows are offered no surface and refused not_offered", async () => {
|
|
749
|
+
const testRenderer = createRenderer();
|
|
750
|
+
const host = createFakeSurfaceHost();
|
|
751
|
+
const logger = createLogger();
|
|
752
|
+
const zonePresentation = createPresentation({
|
|
753
|
+
id: 'z1',
|
|
754
|
+
applicationDeploymentId: 'zone-app',
|
|
755
|
+
applicationVariables: { content: { url: CONTENT_URL } },
|
|
756
|
+
});
|
|
757
|
+
const window = createPresentationWindow({
|
|
758
|
+
windowId: 'multizone',
|
|
759
|
+
presentation: createPresentation({
|
|
760
|
+
id: 'mz',
|
|
761
|
+
r: {
|
|
762
|
+
resource: { updatedAt: 'updatedAt', createdAt: 'createdAt' },
|
|
763
|
+
presentationsPlaylists: [{ playlistId: 'zone-playlist' }],
|
|
764
|
+
},
|
|
765
|
+
}),
|
|
766
|
+
playlistWindowManagerState: createPlaylistWindowManagerState({
|
|
767
|
+
presentations: { z1: zonePresentation },
|
|
768
|
+
applicationVersions: {
|
|
769
|
+
'zone-app': createApplicationVersion({ id: 'zone-app' }),
|
|
770
|
+
},
|
|
771
|
+
}),
|
|
772
|
+
ruleEngineState: createRuleEngineState(
|
|
773
|
+
{
|
|
774
|
+
'zone-playlist': {
|
|
775
|
+
id: 'zone-playlist',
|
|
776
|
+
mode: re.PlaylistMode.Sequential,
|
|
777
|
+
items: [{ presentationId: 'z1' }],
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
{ z1: { id: 'z1', applicationName: 'videos' } }
|
|
781
|
+
),
|
|
782
|
+
renderer: testRenderer,
|
|
783
|
+
logger,
|
|
784
|
+
surfaceBroker: new SurfaceBroker(host.surfaces, logger),
|
|
785
|
+
});
|
|
786
|
+
window.load();
|
|
787
|
+
await delay(0);
|
|
788
|
+
|
|
789
|
+
testRenderer.runReceiveMessageCallback('open_child_window', {
|
|
790
|
+
windowId: 'zone1',
|
|
791
|
+
playlistId: 'zone-playlist',
|
|
792
|
+
x: 0,
|
|
793
|
+
y: 0,
|
|
794
|
+
width: 960,
|
|
795
|
+
height: 1080,
|
|
796
|
+
isMuted: false,
|
|
797
|
+
isFocused: false,
|
|
798
|
+
});
|
|
799
|
+
await delay(0);
|
|
800
|
+
|
|
801
|
+
// The renderer's callbacks now reach the zone's window.
|
|
802
|
+
const zoneWindowId = testRenderer.createWindow.mock.calls[1][0].windowId;
|
|
803
|
+
testRenderer.runReceiveMessageCallback('loaded', undefined);
|
|
804
|
+
const zoneMessages = () =>
|
|
805
|
+
testRenderer.sendMessageString.mock.calls
|
|
806
|
+
.filter(([windowId]) => windowId === zoneWindowId)
|
|
807
|
+
.map(([, message]) => JSON.parse(message));
|
|
808
|
+
expect(
|
|
809
|
+
zoneMessages().find((m) => m.type === 'render').payload
|
|
810
|
+
).not.toHaveProperty('nativeVideo');
|
|
811
|
+
|
|
812
|
+
testRenderer.runReceiveMessageCallback('open_video', {
|
|
813
|
+
url: CONTENT_URL,
|
|
814
|
+
bounds: { x: 0, y: 0, width: 960, height: 1080 },
|
|
815
|
+
muted: false,
|
|
816
|
+
});
|
|
817
|
+
await delay(0);
|
|
818
|
+
|
|
819
|
+
expect(
|
|
820
|
+
zoneMessages()
|
|
821
|
+
.filter((m) => m.type === 'video_refused')
|
|
822
|
+
.map((m) => m.payload.reason)
|
|
823
|
+
).toEqual(['not_offered']);
|
|
824
|
+
expect(host.calls).toEqual([]);
|
|
825
|
+
await window.remove();
|
|
826
|
+
});
|
|
827
|
+
});
|