@applicaster/quick-brick-player 16.0.0-rc.6 → 16.0.0-rc.61
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 +6 -6
- package/src/Player/AudioLayer/AudioDescription.tsx +46 -11
- package/src/Player/AudioLayer/AudioPlayerWrapper.tsx +38 -48
- package/src/Player/AudioLayer/Layout/Controls/__tests__/utils.test.ts +66 -0
- package/src/Player/AudioLayer/Layout/Controls/index.tsx +34 -14
- package/src/Player/AudioLayer/Layout/Controls/utils.ts +26 -0
- package/src/Player/AudioLayer/Layout/DockedControls/index.tsx +1 -13
- package/src/Player/AudioLayer/Layout/MobileLayout.tsx +0 -4
- package/src/Player/AudioLayer/Layout/TabletLandscapeLayout.tsx +0 -4
- package/src/Player/AudioLayer/Layout/TabletPortraitLayout.tsx +0 -4
- package/src/Player/AudioLayer/SleepTimerManager.ts +5 -1
- package/src/Player/AudioLayer/SpeedManager.ts +4 -0
- package/src/Player/AudioLayer/__tests__/AudioDescription.test.tsx +147 -0
- package/src/Player/AudioLayer/__tests__/AudioPlayerWrapper.test.tsx +130 -0
- package/src/Player/AudioLayer/__tests__/SleepTimerManager.test.ts +78 -0
- package/src/Player/AudioLayer/__tests__/SpeedManager.test.ts +57 -0
- package/src/Player/AudioLayer/__tests__/playerActions.test.ts +351 -0
- package/src/Player/AudioLayer/__tests__/usePlaylistNavigation.test.ts +387 -0
- package/src/Player/AudioLayer/components/SleepTimerModalHeader/SleepTimerModalHeader.tsx +9 -8
- package/src/Player/AudioLayer/components/SleepTimerModalHeader/__tests__/SleepTimerModalHeader.test.tsx +53 -0
- package/src/Player/AudioLayer/playerActions.ts +280 -0
- package/src/Player/AudioLayer/usePlaylistNavigation.ts +134 -0
- package/src/Player/AudioLayer/useSleepTimerModal.ts +20 -57
- package/src/Player/__tests__/getNativeProps.test.ts +115 -0
- package/src/Player/__tests__/toNativeMetadata.test.ts +77 -0
- package/src/Player/index.tsx +72 -27
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { renderHook, act } from "@testing-library/react-native";
|
|
2
|
+
import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
3
|
+
import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
|
|
4
|
+
import { loadFeedEntry } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/OverlayObserver/utils";
|
|
5
|
+
|
|
6
|
+
import { usePlaylistNavigation } from "../usePlaylistNavigation";
|
|
7
|
+
|
|
8
|
+
jest.mock("@applicaster/zapp-react-native-utils/reactHooks", () => ({
|
|
9
|
+
useNavigation: jest.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
jest.mock(
|
|
13
|
+
"@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer",
|
|
14
|
+
() => ({ usePlayer: jest.fn() })
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
jest.mock(
|
|
18
|
+
"@applicaster/zapp-react-native-utils/appUtils/playerManager/OverlayObserver/utils",
|
|
19
|
+
() => ({
|
|
20
|
+
loadFeedEntry: jest.fn(),
|
|
21
|
+
retrieveFeedUrl: async (entry) => entry?.extensions?.play_next_feed_url,
|
|
22
|
+
retrievePlayPreviousFeedUrl: (entry) =>
|
|
23
|
+
entry?.extensions?.play_previous_feed_url,
|
|
24
|
+
})
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const useNavigationMock = useNavigation as jest.Mock;
|
|
28
|
+
const loadFeedEntryMock = loadFeedEntry as jest.Mock;
|
|
29
|
+
const usePlayerMock = usePlayer as jest.Mock;
|
|
30
|
+
|
|
31
|
+
const NEXT_URL = "https://feeds/next.json";
|
|
32
|
+
const PREV_URL = "https://feeds/prev.json";
|
|
33
|
+
|
|
34
|
+
const entryWith = (extensions: Record<string, unknown>): ZappEntry =>
|
|
35
|
+
({ id: "current", type: { value: "audio" }, extensions }) as ZappEntry;
|
|
36
|
+
|
|
37
|
+
/** Seekable VOD by default; pass 0 for a live stream with no seek window. */
|
|
38
|
+
const stateAt = (
|
|
39
|
+
currentTime: number,
|
|
40
|
+
seekableDuration = 600
|
|
41
|
+
): QuickBrickPlayer.PlayerState =>
|
|
42
|
+
({ currentTime, seekableDuration }) as QuickBrickPlayer.PlayerState;
|
|
43
|
+
|
|
44
|
+
const NOT_SEEKABLE = 0;
|
|
45
|
+
|
|
46
|
+
const renderPlaylistNavigation = async (
|
|
47
|
+
entry: ZappEntry,
|
|
48
|
+
playerState: QuickBrickPlayer.PlayerState
|
|
49
|
+
) => {
|
|
50
|
+
const rendered = renderHook(() => usePlaylistNavigation(entry, playerState));
|
|
51
|
+
|
|
52
|
+
await act(async () => {});
|
|
53
|
+
|
|
54
|
+
return rendered;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
describe("usePlaylistNavigation", () => {
|
|
58
|
+
let replaceTop: jest.Mock;
|
|
59
|
+
let seekTo: jest.Mock;
|
|
60
|
+
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
replaceTop = jest.fn();
|
|
63
|
+
seekTo = jest.fn();
|
|
64
|
+
|
|
65
|
+
useNavigationMock.mockReturnValue({ replaceTop });
|
|
66
|
+
|
|
67
|
+
usePlayerMock.mockReturnValue({ seekTo });
|
|
68
|
+
|
|
69
|
+
loadFeedEntryMock.mockReset();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("availability", () => {
|
|
73
|
+
it("reports both directions when both extensions are present", async () => {
|
|
74
|
+
const entry = entryWith({
|
|
75
|
+
play_next_feed_url: NEXT_URL,
|
|
76
|
+
play_previous_feed_url: PREV_URL,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const { result } = await renderPlaylistNavigation(entry, stateAt(0));
|
|
80
|
+
|
|
81
|
+
expect(result.current.hasNext).toBe(true);
|
|
82
|
+
expect(result.current.hasPrevious).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("keeps previous available at the head of a chain when the track is seekable", async () => {
|
|
86
|
+
const entry = entryWith({ play_next_feed_url: NEXT_URL });
|
|
87
|
+
|
|
88
|
+
const { result } = await renderPlaylistNavigation(entry, stateAt(0));
|
|
89
|
+
|
|
90
|
+
expect(result.current.hasNext).toBe(true);
|
|
91
|
+
expect(result.current.hasPrevious).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("reports no previous only when the track cannot seek either", async () => {
|
|
95
|
+
const entry = entryWith({ play_next_feed_url: NEXT_URL });
|
|
96
|
+
|
|
97
|
+
const { result } = await renderPlaylistNavigation(
|
|
98
|
+
entry,
|
|
99
|
+
stateAt(0, NOT_SEEKABLE)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(result.current.hasNext).toBe(true);
|
|
103
|
+
expect(result.current.hasPrevious).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("reports neither direction for a non-seekable entry with no extensions", async () => {
|
|
107
|
+
const { result } = await renderPlaylistNavigation(
|
|
108
|
+
entryWith({}),
|
|
109
|
+
stateAt(0, NOT_SEEKABLE)
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
expect(result.current.hasNext).toBe(false);
|
|
113
|
+
expect(result.current.hasPrevious).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("goNext", () => {
|
|
118
|
+
it("plays the entry resolved from the next feed url", async () => {
|
|
119
|
+
const target = { id: "next" } as ZappEntry;
|
|
120
|
+
loadFeedEntryMock.mockResolvedValue(target);
|
|
121
|
+
const entry = entryWith({ play_next_feed_url: NEXT_URL });
|
|
122
|
+
|
|
123
|
+
const { result } = await renderPlaylistNavigation(entry, stateAt(0));
|
|
124
|
+
|
|
125
|
+
await act(async () => result.current.goNext());
|
|
126
|
+
|
|
127
|
+
expect(loadFeedEntryMock).toHaveBeenCalledWith(NEXT_URL, entry);
|
|
128
|
+
expect(replaceTop).toHaveBeenCalledWith(target);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("does nothing when there is no next url", async () => {
|
|
132
|
+
const { result } = renderHook(() =>
|
|
133
|
+
usePlaylistNavigation(entryWith({}), stateAt(0))
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
await act(async () => result.current.goNext());
|
|
137
|
+
|
|
138
|
+
expect(loadFeedEntryMock).not.toHaveBeenCalled();
|
|
139
|
+
expect(replaceTop).not.toHaveBeenCalled();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("leaves playback untouched when the feed fails to load", async () => {
|
|
143
|
+
loadFeedEntryMock.mockRejectedValue(new Error("network down"));
|
|
144
|
+
const entry = entryWith({ play_next_feed_url: NEXT_URL });
|
|
145
|
+
|
|
146
|
+
const { result } = renderHook(() =>
|
|
147
|
+
usePlaylistNavigation(entry, stateAt(0))
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
await act(async () => result.current.goNext());
|
|
151
|
+
|
|
152
|
+
expect(replaceTop).not.toHaveBeenCalled();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("goPrevious", () => {
|
|
157
|
+
it("restarts the current track when past the threshold", async () => {
|
|
158
|
+
const entry = entryWith({ play_previous_feed_url: PREV_URL });
|
|
159
|
+
|
|
160
|
+
const { result } = renderHook(() =>
|
|
161
|
+
usePlaylistNavigation(entry, stateAt(10))
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
await act(async () => result.current.goPrevious());
|
|
165
|
+
|
|
166
|
+
expect(seekTo).toHaveBeenCalledWith(0);
|
|
167
|
+
expect(replaceTop).not.toHaveBeenCalled();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("plays the previous entry when before the threshold", async () => {
|
|
171
|
+
const target = { id: "prev" } as ZappEntry;
|
|
172
|
+
loadFeedEntryMock.mockResolvedValue(target);
|
|
173
|
+
const entry = entryWith({ play_previous_feed_url: PREV_URL });
|
|
174
|
+
|
|
175
|
+
const { result } = renderHook(() =>
|
|
176
|
+
usePlaylistNavigation(entry, stateAt(1))
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
await act(async () => result.current.goPrevious());
|
|
180
|
+
|
|
181
|
+
expect(seekTo).not.toHaveBeenCalled();
|
|
182
|
+
expect(replaceTop).toHaveBeenCalledWith(target);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("restarts the track when there is no previous entry to move to", async () => {
|
|
186
|
+
const { result } = renderHook(() =>
|
|
187
|
+
usePlaylistNavigation(
|
|
188
|
+
entryWith({ play_next_feed_url: NEXT_URL }),
|
|
189
|
+
stateAt(390)
|
|
190
|
+
)
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
await act(async () => result.current.goPrevious());
|
|
194
|
+
|
|
195
|
+
expect(seekTo).toHaveBeenCalledWith(0);
|
|
196
|
+
expect(replaceTop).not.toHaveBeenCalled();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("restarts even before the threshold when there is no previous entry", async () => {
|
|
200
|
+
const { result } = renderHook(() =>
|
|
201
|
+
usePlaylistNavigation(entryWith({}), stateAt(1))
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
await act(async () => result.current.goPrevious());
|
|
205
|
+
|
|
206
|
+
expect(seekTo).toHaveBeenCalledWith(0);
|
|
207
|
+
expect(replaceTop).not.toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// usePlayer, not playerManager.getActivePlayer — the latter is always the
|
|
211
|
+
// local player, so restarting would be lost while casting.
|
|
212
|
+
it("restarts through the player that owns playback, not the local one", async () => {
|
|
213
|
+
const receiverSeekTo = jest.fn();
|
|
214
|
+
usePlayerMock.mockReturnValue({ seekTo: receiverSeekTo });
|
|
215
|
+
|
|
216
|
+
const { result } = renderHook(() =>
|
|
217
|
+
usePlaylistNavigation(
|
|
218
|
+
entryWith({ play_previous_feed_url: PREV_URL }),
|
|
219
|
+
stateAt(10)
|
|
220
|
+
)
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
await act(async () => result.current.goPrevious());
|
|
224
|
+
|
|
225
|
+
expect(receiverSeekTo).toHaveBeenCalledWith(0);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("does nothing when there is neither a previous entry nor a seek window", async () => {
|
|
229
|
+
const { result } = renderHook(() =>
|
|
230
|
+
usePlaylistNavigation(entryWith({}), stateAt(10, NOT_SEEKABLE))
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
await act(async () => result.current.goPrevious());
|
|
234
|
+
|
|
235
|
+
expect(seekTo).not.toHaveBeenCalled();
|
|
236
|
+
expect(replaceTop).not.toHaveBeenCalled();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("navigates to the previous entry past the threshold when the stream cannot seek", async () => {
|
|
240
|
+
const target = { id: "prev" } as ZappEntry;
|
|
241
|
+
loadFeedEntryMock.mockResolvedValue(target);
|
|
242
|
+
|
|
243
|
+
const { result } = renderHook(() =>
|
|
244
|
+
usePlaylistNavigation(
|
|
245
|
+
entryWith({ play_previous_feed_url: PREV_URL }),
|
|
246
|
+
stateAt(10, NOT_SEEKABLE)
|
|
247
|
+
)
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
await act(async () => result.current.goPrevious());
|
|
251
|
+
|
|
252
|
+
expect(seekTo).not.toHaveBeenCalled();
|
|
253
|
+
expect(replaceTop).toHaveBeenCalledWith(target);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("restarts past the threshold when the stream is seekable", async () => {
|
|
257
|
+
const { result } = renderHook(() =>
|
|
258
|
+
usePlaylistNavigation(
|
|
259
|
+
entryWith({ play_previous_feed_url: PREV_URL }),
|
|
260
|
+
stateAt(10)
|
|
261
|
+
)
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
await act(async () => result.current.goPrevious());
|
|
265
|
+
|
|
266
|
+
expect(seekTo).toHaveBeenCalledWith(0);
|
|
267
|
+
expect(replaceTop).not.toHaveBeenCalled();
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe("double tap guard", () => {
|
|
272
|
+
const bothDirections = {
|
|
273
|
+
play_next_feed_url: NEXT_URL,
|
|
274
|
+
play_previous_feed_url: PREV_URL,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Makes the next feed request hang until the returned function is called,
|
|
279
|
+
* so a test can fire a second tap while the first is still in flight.
|
|
280
|
+
*/
|
|
281
|
+
const deferredFeed = () => {
|
|
282
|
+
const deferred: { complete?: (entry: ZappEntry) => void } = {};
|
|
283
|
+
|
|
284
|
+
loadFeedEntryMock.mockReturnValue(
|
|
285
|
+
new Promise<ZappEntry>((resolve) => {
|
|
286
|
+
deferred.complete = resolve;
|
|
287
|
+
})
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
return (entry: ZappEntry) => deferred.complete(entry);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
it("ignores a second tap while the first request is still in flight", async () => {
|
|
294
|
+
const complete = deferredFeed();
|
|
295
|
+
|
|
296
|
+
const { result } = await renderPlaylistNavigation(
|
|
297
|
+
entryWith(bothDirections),
|
|
298
|
+
stateAt(0)
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
await act(async () => {
|
|
302
|
+
const first = result.current.goNext();
|
|
303
|
+
const second = result.current.goNext();
|
|
304
|
+
|
|
305
|
+
complete({ id: "next" } as ZappEntry);
|
|
306
|
+
|
|
307
|
+
await Promise.all([first, second]);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
expect(loadFeedEntryMock).toHaveBeenCalledTimes(1);
|
|
311
|
+
expect(replaceTop).toHaveBeenCalledTimes(1);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("stops Previous racing an in-flight Next", async () => {
|
|
315
|
+
const complete = deferredFeed();
|
|
316
|
+
|
|
317
|
+
const { result } = await renderPlaylistNavigation(
|
|
318
|
+
entryWith(bothDirections),
|
|
319
|
+
stateAt(0)
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
await act(async () => {
|
|
323
|
+
const next = result.current.goNext();
|
|
324
|
+
const previous = result.current.goPrevious();
|
|
325
|
+
|
|
326
|
+
complete({ id: "next" } as ZappEntry);
|
|
327
|
+
|
|
328
|
+
await Promise.all([next, previous]);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
expect(loadFeedEntryMock).toHaveBeenCalledTimes(1);
|
|
332
|
+
|
|
333
|
+
expect(loadFeedEntryMock).toHaveBeenCalledWith(
|
|
334
|
+
NEXT_URL,
|
|
335
|
+
expect.anything()
|
|
336
|
+
);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("allows a new navigation once the previous one has completed", async () => {
|
|
340
|
+
loadFeedEntryMock.mockResolvedValue({ id: "next" } as ZappEntry);
|
|
341
|
+
|
|
342
|
+
const { result } = await renderPlaylistNavigation(
|
|
343
|
+
entryWith(bothDirections),
|
|
344
|
+
stateAt(0)
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
await act(async () => result.current.goNext());
|
|
348
|
+
await act(async () => result.current.goNext());
|
|
349
|
+
|
|
350
|
+
expect(loadFeedEntryMock).toHaveBeenCalledTimes(2);
|
|
351
|
+
expect(replaceTop).toHaveBeenCalledTimes(2);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("releases the guard after a failed request so the control can retry", async () => {
|
|
355
|
+
loadFeedEntryMock
|
|
356
|
+
.mockRejectedValueOnce(new Error("network down"))
|
|
357
|
+
.mockResolvedValueOnce({ id: "next" } as ZappEntry);
|
|
358
|
+
|
|
359
|
+
const { result } = await renderPlaylistNavigation(
|
|
360
|
+
entryWith(bothDirections),
|
|
361
|
+
stateAt(0)
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
await act(async () => result.current.goNext());
|
|
365
|
+
|
|
366
|
+
expect(replaceTop).not.toHaveBeenCalled();
|
|
367
|
+
|
|
368
|
+
await act(async () => result.current.goNext());
|
|
369
|
+
|
|
370
|
+
expect(replaceTop).toHaveBeenCalledTimes(1);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("does not guard the Previous restart, which is local and instant", async () => {
|
|
374
|
+
loadFeedEntryMock.mockResolvedValue({ id: "prev" } as ZappEntry);
|
|
375
|
+
|
|
376
|
+
const { result } = renderHook(() =>
|
|
377
|
+
usePlaylistNavigation(entryWith(bothDirections), stateAt(10))
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
await act(async () => result.current.goPrevious());
|
|
381
|
+
await act(async () => result.current.goPrevious());
|
|
382
|
+
|
|
383
|
+
expect(seekTo).toHaveBeenCalledTimes(2);
|
|
384
|
+
expect(loadFeedEntryMock).not.toHaveBeenCalled();
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { ModalHeader } from "@applicaster/zapp-react-native-ui-components/Components/ModalComponent/Header";
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
type SleepTimerModalHeaderProps = React.ComponentProps<typeof ModalHeader> & {
|
|
5
|
+
timerDate?: number | null;
|
|
6
|
+
};
|
|
4
7
|
|
|
5
8
|
const formatTime = (milliseconds: number) => {
|
|
6
9
|
const totalSeconds = Math.floor(milliseconds / 1000);
|
|
@@ -20,12 +23,10 @@ const formatTime = (milliseconds: number) => {
|
|
|
20
23
|
};
|
|
21
24
|
|
|
22
25
|
export const SleepTimerModalHeader = ({
|
|
23
|
-
|
|
26
|
+
timerDate,
|
|
24
27
|
...props
|
|
25
|
-
}: {
|
|
26
|
-
|
|
27
|
-
}) => {
|
|
28
|
-
const sleepTimerDate = +summary;
|
|
28
|
+
}: SleepTimerModalHeaderProps) => {
|
|
29
|
+
const sleepTimerDate = timerDate || 0;
|
|
29
30
|
|
|
30
31
|
const [timeLeft, setTimeLeft] = React.useState(
|
|
31
32
|
sleepTimerDate !== 0 ? sleepTimerDate - Date.now() : 0
|
|
@@ -59,8 +60,8 @@ export const SleepTimerModalHeader = ({
|
|
|
59
60
|
|
|
60
61
|
return (
|
|
61
62
|
<ModalHeader
|
|
62
|
-
|
|
63
|
-
{
|
|
63
|
+
{...props}
|
|
64
|
+
summary={showCounter && timeLeft > 0 ? formatTime(timeLeft) : undefined}
|
|
64
65
|
/>
|
|
65
66
|
);
|
|
66
67
|
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "@testing-library/react-native";
|
|
3
|
+
import { ModalHeader } from "@applicaster/zapp-react-native-ui-components/Components/ModalComponent/Header";
|
|
4
|
+
import { SleepTimerModalHeader } from "../SleepTimerModalHeader";
|
|
5
|
+
|
|
6
|
+
jest.mock(
|
|
7
|
+
"@applicaster/zapp-react-native-ui-components/Components/ModalComponent/Header",
|
|
8
|
+
() => ({
|
|
9
|
+
ModalHeader: jest.fn(() => null),
|
|
10
|
+
})
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
describe("SleepTimerModalHeader", () => {
|
|
14
|
+
const headerProps = {
|
|
15
|
+
dismiss: jest.fn(),
|
|
16
|
+
configuration: {} as React.ComponentProps<
|
|
17
|
+
typeof ModalHeader
|
|
18
|
+
>["configuration"],
|
|
19
|
+
title: "Sleep Timer",
|
|
20
|
+
summary: undefined as string | undefined,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
jest.clearAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("passes a MM:SS summary when timerDate is in the future", () => {
|
|
28
|
+
const timerDate = Date.now() + 90 * 1000;
|
|
29
|
+
|
|
30
|
+
render(<SleepTimerModalHeader timerDate={timerDate} {...headerProps} />);
|
|
31
|
+
|
|
32
|
+
expect(ModalHeader).toHaveBeenCalledWith(
|
|
33
|
+
expect.objectContaining({
|
|
34
|
+
title: "Sleep Timer",
|
|
35
|
+
summary: expect.stringMatching(/^\d{2}:\d{2}$/),
|
|
36
|
+
}),
|
|
37
|
+
expect.anything()
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("does not show a 00:00 summary when timerDate is in the past", () => {
|
|
42
|
+
render(
|
|
43
|
+
<SleepTimerModalHeader timerDate={Date.now() - 1000} {...headerProps} />
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(ModalHeader).toHaveBeenCalledWith(
|
|
47
|
+
expect.objectContaining({
|
|
48
|
+
summary: undefined,
|
|
49
|
+
}),
|
|
50
|
+
expect.anything()
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
});
|