@applicaster/quick-brick-player 15.1.10-rc.0 → 15.1.11-alpha.7557142055

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/quick-brick-player",
3
- "version": "15.1.10-rc.0",
3
+ "version": "15.1.11-alpha.7557142055",
4
4
  "description": "Quick Brick Player",
5
5
  "main": "./src/index.ts",
6
6
  "types": "index.d.ts",
@@ -37,9 +37,9 @@
37
37
  "dependencies": {
38
38
  "@applicaster/quick-brick-mobile-transport-controls": "15.1.9",
39
39
  "@applicaster/quick-brick-tv-transport-controls": "15.1.9",
40
- "@applicaster/zapp-react-native-tvos-app": "15.1.10-rc.0",
41
- "@applicaster/zapp-react-native-ui-components": "15.1.10-rc.0",
42
- "@applicaster/zapp-react-native-utils": "15.1.10-rc.0",
40
+ "@applicaster/zapp-react-native-tvos-app": "15.1.11-alpha.7557142055",
41
+ "@applicaster/zapp-react-native-ui-components": "15.1.11-alpha.7557142055",
42
+ "@applicaster/zapp-react-native-utils": "15.1.11-alpha.7557142055",
43
43
  "query-string": "7.1.3",
44
44
  "shaka-player": "4.3.5",
45
45
  "typeface-montserrat": "^0.0.54",
@@ -12,23 +12,18 @@ import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
12
12
  import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
13
13
  import { PlayNextData } from "@applicaster/zapp-react-native-ui-components/Components/PlayerContainer/PlayerContainer";
14
14
  import { formatSpeed } from "./utils";
15
- import {
16
- getBottomSheetModalDefaultItemIcon,
17
- getBottomSheetModalIconProps,
18
- getBottomSheetModalItemProps,
19
- getBottomSheetModalLabelProps,
20
- getBottomSheetModalSelectedItemIcon,
21
- } from "../Utils";
22
15
 
23
16
  import { toNumberWithDefault } from "@applicaster/zapp-react-native-utils/numberUtils";
24
-
25
17
  import { DEFAULT_SPEED, PlaybackSpeed, SpeedController } from "./SpeedManager";
26
18
  import {
27
19
  dismissModal as dismissBottomSheetModal,
28
- openBottomSheetModal,
29
20
  useModalStoreState,
30
21
  } from "@applicaster/zapp-react-native-utils/modalState";
31
- import { speedOptions } from "../Const/speedOptions";
22
+ import { uiActionsRegistry } from "@applicaster/zapp-react-native-utils/uiActionsRegistrator";
23
+ import {
24
+ PLAYER_ACTION_IDENTIFIERS,
25
+ registerPlayerActions,
26
+ } from "./playerActions";
32
27
 
33
28
  const { log_debug } = createLogger({
34
29
  subsystem: "UIComponent",
@@ -118,6 +113,14 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
118
113
  setSpeedManager(player ? new SpeedController(player) : null);
119
114
  }, [player]);
120
115
 
116
+ useEffect(() => {
117
+ if (!speedManager || playerState.isLive) {
118
+ return;
119
+ }
120
+
121
+ return registerPlayerActions({ speedController: speedManager });
122
+ }, [speedManager, playerState.isLive]);
123
+
121
124
  useEffect(() => {
122
125
  if (!player) return;
123
126
 
@@ -216,36 +219,11 @@ export const AudioPlayerWrapper: React.FC<Props> = (props: Props) => {
216
219
  player?.forward(seekDuration);
217
220
  }, [seekDuration, player]);
218
221
 
219
- const onSpeedSelect = React.useCallback(
220
- async (item) => {
221
- await speedManager?.setPlaybackSpeed(item);
222
- },
223
- [speedManager]
224
- );
225
-
226
222
  const onSpeedButtonPress = React.useCallback(() => {
227
- openBottomSheetModal({
228
- modalBottomSheetContentProps: {
229
- items: speedOptions,
230
- current_selection: playbackSpeed,
231
- onPress: onSpeedSelect,
232
- title: configuration.playback_speed_title,
233
- itemProps: getBottomSheetModalItemProps,
234
- iconProps: getBottomSheetModalIconProps,
235
- labelProps: getBottomSheetModalLabelProps,
236
- getSelectedItemIcon: getBottomSheetModalSelectedItemIcon,
237
- getDefaultItemIcon: getBottomSheetModalDefaultItemIcon,
238
- iconPlacement: "right",
239
- },
240
- options: {
241
- supportedOrientations: [
242
- "portrait",
243
- "landscape",
244
- "portrait-upside-down",
245
- ],
246
- },
247
- });
248
- }, [playbackSpeed, onSpeedSelect, configuration.playback_speed_title]);
223
+ uiActionsRegistry
224
+ .getAction(PLAYER_ACTION_IDENTIFIERS.PLAYBACK_SPEED)
225
+ ?.invokeAction(entry);
226
+ }, [entry]);
249
227
 
250
228
  const onPlayerSeek = React.useCallback(
251
229
  (targetTime: number) => {
@@ -2,10 +2,7 @@ import * as React from "react";
2
2
  import { StyleSheet, View } from "react-native";
3
3
 
4
4
  import { dayjs } from "@applicaster/zapp-react-native-utils/dateUtils";
5
- import {
6
- useIsTablet,
7
- useZStore,
8
- } from "@applicaster/zapp-react-native-utils/reactHooks";
5
+ import { useIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks";
9
6
  import {
10
7
  Controls as MobileTransportControls,
11
8
  ProgressBar,
@@ -100,11 +97,8 @@ export const Controls = (props: Props) => {
100
97
  maxWidth = 320,
101
98
  } = props;
102
99
 
103
- const configuration = useZStore("playerConfiguration");
104
-
105
100
  const { sleepTimerState, onSleepButtonPress } = useSleepTimerModal(
106
- playerManager.getActivePlayer(),
107
- configuration
101
+ playerManager.getActivePlayer()
108
102
  );
109
103
 
110
104
  const { hasNext, hasPrevious, goNext, goPrevious } = usePlaylistNavigation(
@@ -41,6 +41,10 @@ export class SleepTimerController {
41
41
  return this.sleepTimerStateSubject.asObservable();
42
42
  }
43
43
 
44
+ get currentState(): SleepTimerState {
45
+ return this.sleepTimerStateSubject.getValue();
46
+ }
47
+
44
48
  private async scheduleSleep(sleepDate): Promise<boolean> {
45
49
  log_debug(`Setting sleep interval to ${sleepDate}`);
46
50
 
@@ -118,7 +122,7 @@ export class SleepTimerController {
118
122
 
119
123
  log_debug("Sleep state restored");
120
124
 
121
- this.setSleepTimer(item as SleepTimer, futureSleepDate);
125
+ await this.setSleepTimer(item as SleepTimer, futureSleepDate);
122
126
  }
123
127
 
124
128
  // Will be called if user closed the player explicitly.
@@ -33,6 +33,10 @@ export class SpeedController {
33
33
  return this.playbackSpeedSubject.asObservable();
34
34
  }
35
35
 
36
+ get currentSpeed(): PlaybackSpeed {
37
+ return this.playbackSpeedSubject.getValue();
38
+ }
39
+
36
40
  async loadSpeed() {
37
41
  const speed = await localStorage.getItem(SPEED_PREFERENCE_KEY, NAMESPACE);
38
42
 
@@ -0,0 +1,78 @@
1
+ import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage";
2
+ import { SLEEP_TIMER_OFF, SleepTimerController } from "../SleepTimerManager";
3
+
4
+ jest.mock(
5
+ "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage",
6
+ () => ({
7
+ localStorage: {
8
+ getItem: jest.fn(),
9
+ setItem: jest.fn(),
10
+ removeItem: jest.fn(),
11
+ },
12
+ })
13
+ );
14
+
15
+ describe("SleepTimerController", () => {
16
+ const player = {
17
+ startSleepTimer: jest.fn(),
18
+ cancelSleepTimer: jest.fn(),
19
+ };
20
+
21
+ let sleepController: SleepTimerController;
22
+
23
+ beforeEach(() => {
24
+ jest.clearAllMocks();
25
+ (localStorage.setItem as jest.Mock).mockResolvedValue(undefined);
26
+ (localStorage.removeItem as jest.Mock).mockResolvedValue(undefined);
27
+ (localStorage.getItem as jest.Mock).mockResolvedValue(undefined);
28
+ sleepController = new SleepTimerController(player as any);
29
+ });
30
+
31
+ it("starts from the off state", () => {
32
+ expect(sleepController.currentState.timer).toEqual(SLEEP_TIMER_OFF);
33
+ });
34
+
35
+ it("starts a sleep timer on the constructed player", async () => {
36
+ await sleepController.setSleepTimer({
37
+ label: "15 minutes",
38
+ value: 15,
39
+ });
40
+
41
+ expect(player.cancelSleepTimer).toHaveBeenCalled();
42
+ expect(player.startSleepTimer).toHaveBeenCalled();
43
+ expect(sleepController.currentState.timer.value).toBe(15);
44
+ });
45
+
46
+ it("loadSleep restores an unexpired timer", async () => {
47
+ const futureSleepDate = Date.now() + 60 * 1000;
48
+
49
+ (localStorage.getItem as jest.Mock).mockResolvedValue({
50
+ label: "15 minutes",
51
+ value: 15,
52
+ futureSleepDate,
53
+ });
54
+
55
+ player.startSleepTimer.mockClear();
56
+
57
+ await sleepController.loadSleep();
58
+
59
+ expect(player.startSleepTimer).toHaveBeenCalledWith(futureSleepDate);
60
+ expect(sleepController.currentState.timer.value).toBe(15);
61
+ });
62
+
63
+ it("loadSleep drops an expired timer without starting a native timer", async () => {
64
+ (localStorage.getItem as jest.Mock).mockResolvedValue({
65
+ label: "15 minutes",
66
+ value: 15,
67
+ futureSleepDate: Date.now() - 1000,
68
+ });
69
+
70
+ player.startSleepTimer.mockClear();
71
+
72
+ await sleepController.loadSleep();
73
+
74
+ expect(player.startSleepTimer).not.toHaveBeenCalled();
75
+ expect(localStorage.removeItem).toHaveBeenCalled();
76
+ expect(sleepController.currentState.timer).toEqual(SLEEP_TIMER_OFF);
77
+ });
78
+ });
@@ -0,0 +1,57 @@
1
+ import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage";
2
+ import { DEFAULT_SPEED, SpeedController } from "../SpeedManager";
3
+
4
+ jest.mock(
5
+ "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage",
6
+ () => ({
7
+ localStorage: {
8
+ getItem: jest.fn(),
9
+ setItem: jest.fn(),
10
+ removeItem: jest.fn(),
11
+ },
12
+ })
13
+ );
14
+
15
+ describe("SpeedController", () => {
16
+ const player = { setPlaybackRate: jest.fn() };
17
+ let speedController: SpeedController;
18
+
19
+ beforeEach(() => {
20
+ jest.clearAllMocks();
21
+ (localStorage.setItem as jest.Mock).mockResolvedValue(undefined);
22
+ (localStorage.getItem as jest.Mock).mockResolvedValue(undefined);
23
+ speedController = new SpeedController(player as any);
24
+ });
25
+
26
+ it("requires a player in the constructor", () => {
27
+ expect(speedController.currentSpeed).toEqual(DEFAULT_SPEED);
28
+ });
29
+
30
+ it("applies playback speed on the constructed player", async () => {
31
+ await speedController.setPlaybackSpeed({
32
+ asset: "speed_1_5",
33
+ value: 1.5,
34
+ });
35
+
36
+ expect(player.setPlaybackRate).toHaveBeenCalledWith(1.5);
37
+
38
+ expect(speedController.currentSpeed).toEqual({
39
+ asset: "speed_1_5",
40
+ value: 1.5,
41
+ });
42
+ });
43
+
44
+ it("loadSpeed restores a stored rate onto the player", async () => {
45
+ (localStorage.getItem as jest.Mock).mockResolvedValue({
46
+ asset: "speed_1_5",
47
+ value: 1.5,
48
+ });
49
+
50
+ player.setPlaybackRate.mockClear();
51
+
52
+ await speedController.loadSpeed();
53
+
54
+ expect(player.setPlaybackRate).toHaveBeenCalledWith(1.5);
55
+ expect(speedController.currentSpeed.value).toBe(1.5);
56
+ });
57
+ });
@@ -0,0 +1,342 @@
1
+ import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils";
2
+ import {
3
+ actionExecutor,
4
+ ActionResult,
5
+ } from "@applicaster/zapp-react-native-utils/actionsExecutor/ActionExecutor";
6
+ import { uiActionsRegistry } from "@applicaster/zapp-react-native-utils/uiActionsRegistrator";
7
+ import { modalOrchestrator } from "@applicaster/zapp-react-native-utils/modalState/ModalOrchestrator";
8
+ import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage";
9
+ import { firstValueFrom } from "rxjs";
10
+
11
+ import {
12
+ PLAYER_ACTION_IDENTIFIERS,
13
+ PLAYER_ACTION_TYPES,
14
+ registerPlayerActions,
15
+ } from "../playerActions";
16
+ import { SpeedController } from "../SpeedManager";
17
+ import { SleepTimerController } from "../SleepTimerManager";
18
+
19
+ jest.mock("../../Const/sleepTimerOptions", () => ({
20
+ getSleepTimerOptions: (offLabel: string, sleepTimerValue: number | null) => [
21
+ sleepTimerValue ? { label: offLabel, value: 0 } : null,
22
+ { label: "5 minutes", value: 5 },
23
+ { label: "15 minutes", value: 15 },
24
+ ],
25
+ }));
26
+
27
+ jest.mock("@applicaster/zapp-react-native-utils/appUtils", () => ({
28
+ playerManager: {
29
+ getActivePlayer: jest.fn(),
30
+ },
31
+ }));
32
+
33
+ jest.mock(
34
+ "@applicaster/zapp-react-native-utils/modalState/ModalOrchestrator",
35
+ () => ({
36
+ modalOrchestrator: {
37
+ close: jest.fn(),
38
+ stackSize: 0,
39
+ },
40
+ })
41
+ );
42
+
43
+ jest.mock(
44
+ "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage",
45
+ () => ({
46
+ localStorage: {
47
+ getItem: jest.fn(),
48
+ setItem: jest.fn(),
49
+ removeItem: jest.fn(),
50
+ },
51
+ })
52
+ );
53
+
54
+ describe("playerActions", () => {
55
+ const openedSheets: ActionType[] = [];
56
+
57
+ let player: {
58
+ getConfig: () => Record<string, string>;
59
+ setPlaybackRate: jest.Mock;
60
+ startSleepTimer: jest.Mock;
61
+ cancelSleepTimer: jest.Mock;
62
+ };
63
+
64
+ let speedController: SpeedController;
65
+ let sleepController: SleepTimerController;
66
+ let unregisterPlayerActions: () => void;
67
+
68
+ beforeEach(() => {
69
+ openedSheets.length = 0;
70
+ jest.clearAllMocks();
71
+ (localStorage.setItem as jest.Mock).mockResolvedValue(undefined);
72
+ (localStorage.removeItem as jest.Mock).mockResolvedValue(undefined);
73
+
74
+ player = {
75
+ getConfig: () => ({
76
+ playback_speed_title: "Playback Speed",
77
+ playback_sleep_title: "Sleep Timer",
78
+ playback_sleep_off_label: "Off",
79
+ sleep_timer: "sleep_timer",
80
+ sleep_timer_active: "sleep_timer_active",
81
+ }),
82
+ setPlaybackRate: jest.fn(),
83
+ startSleepTimer: jest.fn(),
84
+ cancelSleepTimer: jest.fn(),
85
+ };
86
+
87
+ (playerManager.getActivePlayer as jest.Mock).mockReturnValue(player);
88
+ speedController = new SpeedController(player as any);
89
+ sleepController = new SleepTimerController(player as any);
90
+
91
+ [
92
+ "openBottomSheet",
93
+ PLAYER_ACTION_TYPES.OPEN_PLAYBACK_SPEED,
94
+ PLAYER_ACTION_TYPES.OPEN_SLEEP_TIMER,
95
+ PLAYER_ACTION_TYPES.SET_PLAYBACK_SPEED,
96
+ PLAYER_ACTION_TYPES.SET_SLEEP_TIMER,
97
+ ].forEach((type) => actionExecutor.unregisterAction(type));
98
+
99
+ actionExecutor.registerAction("openBottomSheet", async (action) => {
100
+ openedSheets.push(action);
101
+
102
+ return ActionResult.Success;
103
+ });
104
+
105
+ unregisterPlayerActions = registerPlayerActions({
106
+ speedController,
107
+ sleepController,
108
+ });
109
+ });
110
+
111
+ afterEach(() => {
112
+ unregisterPlayerActions?.();
113
+ });
114
+
115
+ it("does not register actions until a player controller is provided", async () => {
116
+ unregisterPlayerActions();
117
+ unregisterPlayerActions = registerPlayerActions({});
118
+
119
+ const result = await actionExecutor.handleAction({
120
+ type: PLAYER_ACTION_TYPES.SET_PLAYBACK_SPEED,
121
+ options: { value: 1.5, dismissSheet: true },
122
+ });
123
+
124
+ expect(result).toBeUndefined();
125
+ expect(player.setPlaybackRate).not.toHaveBeenCalled();
126
+
127
+ expect(
128
+ uiActionsRegistry.getAction(PLAYER_ACTION_IDENTIFIERS.PLAYBACK_SPEED)
129
+ ).toBeUndefined();
130
+
131
+ expect(
132
+ uiActionsRegistry.getAction(PLAYER_ACTION_IDENTIFIERS.SLEEP_TIMER)
133
+ ).toBeUndefined();
134
+ });
135
+
136
+ it("unregisters player actions when the player goes away", async () => {
137
+ unregisterPlayerActions();
138
+
139
+ expect(
140
+ uiActionsRegistry.getAction(PLAYER_ACTION_IDENTIFIERS.PLAYBACK_SPEED)
141
+ ).toBeUndefined();
142
+
143
+ const result = await actionExecutor.handleAction({
144
+ type: PLAYER_ACTION_TYPES.SET_PLAYBACK_SPEED,
145
+ options: { value: 1.5 },
146
+ });
147
+
148
+ expect(result).toBeUndefined();
149
+ expect(player.setPlaybackRate).not.toHaveBeenCalled();
150
+ });
151
+
152
+ it("opens playback speed through openBottomSheet with the current speed selected", async () => {
153
+ await speedController.setPlaybackSpeed({
154
+ asset: "speed_1",
155
+ value: 1,
156
+ });
157
+
158
+ const result = await actionExecutor.handleAction({
159
+ type: PLAYER_ACTION_TYPES.OPEN_PLAYBACK_SPEED,
160
+ });
161
+
162
+ expect(result).toBe(ActionResult.Success);
163
+
164
+ const sheet = openedSheets[0];
165
+
166
+ expect(sheet.options.replace).toBe(true);
167
+ expect(sheet.options.header.title).toBe("Playback Speed");
168
+ expect(sheet.options.content.role).toBe("collection_selector");
169
+ expect(sheet.options.content.behavior.currentSelection).toEqual(["1"]);
170
+
171
+ expect(sheet.options.content.items.map((item) => item.title)).toEqual([
172
+ "0.8x",
173
+ "1x",
174
+ "1.2x",
175
+ "1.5x",
176
+ "2x",
177
+ ]);
178
+
179
+ expect(
180
+ sheet.options.content.items[0].extensions.tap_actions.actions[0].options
181
+ .dismissSheet
182
+ ).toBe(true);
183
+ });
184
+
185
+ it("opens sleep timer with the ticking countdown header", async () => {
186
+ const timerDate = Date.now() + 15 * 60 * 1000;
187
+
188
+ await sleepController.setSleepTimer(
189
+ { label: "15 minutes", value: 15 },
190
+ timerDate
191
+ );
192
+
193
+ const result = await actionExecutor.handleAction({
194
+ type: PLAYER_ACTION_TYPES.OPEN_SLEEP_TIMER,
195
+ });
196
+
197
+ expect(result).toBe(ActionResult.Success);
198
+
199
+ const sheet = openedSheets[0];
200
+
201
+ expect(sheet.options.replace).toBe(true);
202
+ expect(sheet.options.header.title).toBe("Sleep Timer");
203
+ expect(sheet.options.header.subtitle).toBeUndefined();
204
+ expect(sheet.options.content.behavior.currentSelection).toEqual(["15"]);
205
+
206
+ expect(
207
+ sheet.options.content.items[0].extensions.tap_actions.actions[0].type
208
+ ).toBe(PLAYER_ACTION_TYPES.SET_SLEEP_TIMER);
209
+ });
210
+
211
+ it("setPlaybackSpeed applies the rate and closes the sheet when opened from the picker", async () => {
212
+ const result = await actionExecutor.handleAction({
213
+ type: PLAYER_ACTION_TYPES.SET_PLAYBACK_SPEED,
214
+ options: { value: 1.5, dismissSheet: true },
215
+ });
216
+
217
+ expect(result).toBe(ActionResult.Success);
218
+ expect(player.setPlaybackRate).toHaveBeenCalledWith(1.5);
219
+ expect(localStorage.setItem).toHaveBeenCalled();
220
+ expect(modalOrchestrator.close).toHaveBeenCalled();
221
+ });
222
+
223
+ it("setSleepTimer starts the timer, writes storage, and closes the sheet", async () => {
224
+ const result = await actionExecutor.handleAction({
225
+ type: PLAYER_ACTION_TYPES.SET_SLEEP_TIMER,
226
+ options: { value: 15, label: "15 minutes", dismissSheet: true },
227
+ });
228
+
229
+ expect(result).toBe(ActionResult.Success);
230
+ expect(player.startSleepTimer).toHaveBeenCalled();
231
+ expect(localStorage.setItem).toHaveBeenCalled();
232
+ expect(modalOrchestrator.close).toHaveBeenCalled();
233
+ expect(sleepController.currentState.timer.value).toBe(15);
234
+ });
235
+
236
+ it("setSleepTimer with value 0 cancels the timer", async () => {
237
+ await sleepController.setSleepTimer({
238
+ label: "15 minutes",
239
+ value: 15,
240
+ });
241
+
242
+ player.cancelSleepTimer.mockClear();
243
+ (localStorage.removeItem as jest.Mock).mockClear();
244
+ (modalOrchestrator.close as jest.Mock).mockClear();
245
+
246
+ const result = await actionExecutor.handleAction({
247
+ type: PLAYER_ACTION_TYPES.SET_SLEEP_TIMER,
248
+ options: { value: 0, label: "Off", dismissSheet: true },
249
+ });
250
+
251
+ expect(result).toBe(ActionResult.Success);
252
+ expect(player.cancelSleepTimer).toHaveBeenCalled();
253
+ expect(localStorage.removeItem).toHaveBeenCalled();
254
+ expect(sleepController.currentState.timer.value).toBe(0);
255
+ expect(modalOrchestrator.close).toHaveBeenCalled();
256
+ });
257
+
258
+ it.each([undefined, "foo"])(
259
+ "rejects invalid setPlaybackSpeed value %p",
260
+ async (value) => {
261
+ player.setPlaybackRate.mockClear();
262
+ (localStorage.setItem as jest.Mock).mockClear();
263
+
264
+ const result = await actionExecutor.handleAction({
265
+ type: PLAYER_ACTION_TYPES.SET_PLAYBACK_SPEED,
266
+ options: { value, dismissSheet: true },
267
+ });
268
+
269
+ expect(result).toBe(ActionResult.Error);
270
+ expect(player.setPlaybackRate).not.toHaveBeenCalled();
271
+ expect(localStorage.setItem).not.toHaveBeenCalled();
272
+ expect(modalOrchestrator.close).not.toHaveBeenCalled();
273
+ }
274
+ );
275
+
276
+ it.each([undefined, "foo"])(
277
+ "rejects invalid setSleepTimer value %p",
278
+ async (value) => {
279
+ const result = await actionExecutor.handleAction({
280
+ type: PLAYER_ACTION_TYPES.SET_SLEEP_TIMER,
281
+ options: { value, dismissSheet: true },
282
+ });
283
+
284
+ expect(result).toBe(ActionResult.Error);
285
+ expect(player.startSleepTimer).not.toHaveBeenCalled();
286
+ expect(localStorage.setItem).not.toHaveBeenCalled();
287
+ expect(modalOrchestrator.close).not.toHaveBeenCalled();
288
+ }
289
+ );
290
+
291
+ it("does not close an unrelated sheet when setPlaybackSpeed is invoked standalone", async () => {
292
+ const result = await actionExecutor.handleAction({
293
+ type: PLAYER_ACTION_TYPES.SET_PLAYBACK_SPEED,
294
+ options: { value: 1.5 },
295
+ });
296
+
297
+ expect(result).toBe(ActionResult.Success);
298
+ expect(player.setPlaybackRate).toHaveBeenCalledWith(1.5);
299
+ expect(modalOrchestrator.close).not.toHaveBeenCalled();
300
+ });
301
+
302
+ it("initialEntryState for sleep-timer reflects an active timer", async () => {
303
+ await sleepController.setSleepTimer({
304
+ label: "15 minutes",
305
+ value: 15,
306
+ });
307
+
308
+ const action = uiActionsRegistry.getAction(
309
+ PLAYER_ACTION_IDENTIFIERS.SLEEP_TIMER
310
+ );
311
+
312
+ expect(action.initialEntryState({} as ZappEntry)).toMatchObject({
313
+ state: 1,
314
+ asset: "sleep_timer_active",
315
+ label: "Sleep Timer",
316
+ });
317
+
318
+ const observed = await firstValueFrom(
319
+ action.observeEntryState({} as ZappEntry)
320
+ );
321
+
322
+ expect(observed).toMatchObject({
323
+ state: 1,
324
+ asset: "sleep_timer_active",
325
+ });
326
+ });
327
+
328
+ it("registers UI action identifiers that open the matching sheets", async () => {
329
+ await uiActionsRegistry
330
+ .getAction(PLAYER_ACTION_IDENTIFIERS.PLAYBACK_SPEED)
331
+ .invokeAction({} as ZappEntry);
332
+
333
+ await uiActionsRegistry
334
+ .getAction(PLAYER_ACTION_IDENTIFIERS.SLEEP_TIMER)
335
+ .invokeAction({} as ZappEntry);
336
+
337
+ expect(openedSheets.map((action) => action.options.header.title)).toEqual([
338
+ "Playback Speed",
339
+ "Sleep Timer",
340
+ ]);
341
+ });
342
+ });