@applicaster/zapp-react-native-ui-components 16.0.0-alpha.1593888345 → 16.0.0-alpha.1701179921

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.
Files changed (48) hide show
  1. package/Components/LinkHandler/LinkHandler.tsx +2 -7
  2. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +4 -1
  3. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/ActionButtonController.tsx +19 -4
  4. package/Components/MasterCell/DefaultComponents/ActionButtonsCore/components/__tests__/ActionButtonController.test.tsx +43 -1
  5. package/Components/MasterCell/DefaultComponents/LiveImage/executePreloadHooks.ts +2 -4
  6. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +2 -3
  7. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/factory.test.ts +32 -0
  8. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/index.ts +1 -2
  9. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +4 -2
  10. package/Components/ModalComponent/ActionsBottomSheet/__tests__/openActionsBottomSheet.test.ts +38 -0
  11. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +5 -2
  12. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +4 -1
  13. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +58 -73
  14. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +65 -56
  15. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +140 -65
  16. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +93 -46
  17. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +148 -32
  18. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +113 -0
  19. package/Components/ModalComponent/AudioPlayer/Components/__tests__/NowPlayingHeaderSection.test.tsx +36 -0
  20. package/Components/ModalComponent/AudioPlayer/Components/index.ts +0 -2
  21. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +132 -6
  22. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +202 -13
  23. package/Components/ModalComponent/BottomSheetModalContent.tsx +187 -73
  24. package/Components/ModalComponent/SortableList.tsx +36 -22
  25. package/Components/ModalComponent/TextInputContent.tsx +42 -7
  26. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +111 -6
  27. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +50 -1
  28. package/Components/ModalComponent/index.tsx +7 -9
  29. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +24 -0
  30. package/Components/ModalComponent/presets/selectableCell.tsx +1 -1
  31. package/Components/ModalComponent/showTextInput.ts +11 -2
  32. package/Components/PlayerContainer/PlayerContainer.tsx +8 -1
  33. package/Components/River/ComponentsMap/ComponentsMap.tsx +10 -12
  34. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +138 -75
  35. package/Components/River/__tests__/componentsMap.test.js +8 -3
  36. package/Components/Screen/__tests__/Screen.test.tsx +24 -0
  37. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +2 -0
  38. package/Components/Screen/index.tsx +5 -0
  39. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +1 -1
  40. package/Components/Transitioner/Transitioner.tsx +2 -3
  41. package/Components/Transitioner/__tests__/transitioner.test.js +83 -2
  42. package/Components/VideoLive/LiveImageManager.ts +8 -2
  43. package/Components/VideoLive/__tests__/LiveImageManager.test.ts +59 -0
  44. package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
  45. package/Contexts/ModalNavigationContext/index.tsx +2 -2
  46. package/assets/empty-Info.png +0 -0
  47. package/package.json +5 -5
  48. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +0 -314
@@ -12,14 +12,8 @@
12
12
  */
13
13
 
14
14
  import React from "react";
15
- import {
16
- Image,
17
- Platform,
18
- Pressable,
19
- StyleSheet,
20
- Text,
21
- View,
22
- } from "react-native";
15
+ import { QBImage as Image } from "@applicaster/zapp-react-native-ui-components/Components/Image";
16
+ import { Platform, Pressable, StyleSheet, Text, View } from "react-native";
23
17
  import {
24
18
  MenuItem,
25
19
  SelectionBehavior,
@@ -60,6 +54,8 @@ export type BackgroundConfiguration = {
60
54
  paddingRight?: number;
61
55
  paddingBottom?: number;
62
56
  paddingLeft?: number;
57
+ highlightInsetLeft?: number;
58
+ highlightInsetRight?: number;
63
59
  };
64
60
 
65
61
  export type ImageConfiguration = {
@@ -209,9 +205,9 @@ const CONTAINER_SPEC_LARGE_IMAGE: ContainerSpec = {
209
205
 
210
206
  const CONTAINER_SPEC_DEFAULT: ContainerSpec = {
211
207
  paddingTop: 8,
212
- paddingRight: 20,
208
+ paddingRight: 10,
213
209
  paddingBottom: 8,
214
- paddingLeft: 20,
210
+ paddingLeft: 10,
215
211
  horizontalGutter: 12,
216
212
  verticalGutter: 2,
217
213
  };
@@ -291,13 +287,14 @@ function resolveContainerSpec(image: ImageConfiguration): ContainerSpec {
291
287
 
292
288
  export function mapMenuItemToNowPlayingData(
293
289
  item: MenuItem,
294
- isPaused?: boolean
290
+ isPaused?: boolean,
291
+ nowPlayingLabel = "Now Playing"
295
292
  ): ItemData {
296
293
  return {
297
294
  imageUri: item.icon,
298
295
  textLabel1: item.title,
299
296
  textLabel2: item.summary ?? "",
300
- nowPlayingLabel: "Now Playing",
297
+ nowPlayingLabel,
301
298
  trailingButton: "playPause",
302
299
  isSelected: isPaused !== undefined ? !isPaused : item.isSelected,
303
300
  };
@@ -355,21 +352,30 @@ function ItemContainer({
355
352
  onPress,
356
353
  }: ItemContainerProps) {
357
354
  const container = resolveContainerSpec(image);
358
- const bg = focused ? background.focusedColor : background.defaultColor;
355
+ const insetLeft = background.highlightInsetLeft ?? 0;
356
+ const insetRight = background.highlightInsetRight ?? 0;
359
357
 
360
358
  return (
361
- <Pressable
362
- onPress={onPress}
363
- style={({ pressed }) => ({
364
- backgroundColor: pressed ? background.focusedColor : bg,
365
- borderRadius: background.cornerRadius,
366
- paddingTop: background.paddingTop ?? container.paddingTop,
367
- paddingRight: background.paddingRight ?? container.paddingRight,
368
- paddingBottom: background.paddingBottom ?? container.paddingBottom,
369
- paddingLeft: background.paddingLeft ?? container.paddingLeft,
370
- })}
371
- >
372
- {children}
359
+ <Pressable onPress={onPress}>
360
+ {({ pressed }) => (
361
+ <View
362
+ style={{
363
+ marginLeft: insetLeft,
364
+ marginRight: insetRight,
365
+ backgroundColor:
366
+ pressed || focused
367
+ ? background.focusedColor
368
+ : background.defaultColor,
369
+ borderRadius: background.cornerRadius,
370
+ paddingTop: background.paddingTop ?? container.paddingTop,
371
+ paddingRight: background.paddingRight ?? container.paddingRight,
372
+ paddingBottom: background.paddingBottom ?? container.paddingBottom,
373
+ paddingLeft: background.paddingLeft ?? container.paddingLeft,
374
+ }}
375
+ >
376
+ {children}
377
+ </View>
378
+ )}
373
379
  </Pressable>
374
380
  );
375
381
  }
@@ -402,6 +408,10 @@ type ItemLabelProps = {
402
408
  };
403
409
 
404
410
  function ItemLabel({ text, configuration, focused = false }: ItemLabelProps) {
411
+ if (!text) {
412
+ return null;
413
+ }
414
+
405
415
  const color =
406
416
  focused && configuration.focusedFontColor
407
417
  ? configuration.focusedFontColor
@@ -706,18 +716,31 @@ function ItemButton({
706
716
  }
707
717
  };
708
718
 
719
+ const buttonPadding = {
720
+ paddingTop: layout.paddingTop,
721
+ paddingRight: layout.paddingRight,
722
+ paddingBottom: layout.paddingBottom,
723
+ paddingLeft: layout.paddingLeft,
724
+ };
725
+
726
+ // A nested Pressable with no handler swallows the parent row press.
727
+ if (!onPress) {
728
+ return (
729
+ <View pointerEvents="none" style={buttonPadding}>
730
+ {renderIcon()}
731
+ </View>
732
+ );
733
+ }
734
+
709
735
  return (
710
736
  <Pressable
711
737
  onPress={(e: any) => {
712
738
  e?.stopPropagation?.();
713
- onPress?.();
739
+ onPress();
714
740
  }}
715
741
  style={({ pressed }) => [
742
+ buttonPadding,
716
743
  {
717
- paddingTop: layout.paddingTop,
718
- paddingRight: layout.paddingRight,
719
- paddingBottom: layout.paddingBottom,
720
- paddingLeft: layout.paddingLeft,
721
744
  opacity: pressed || focused ? 0.7 : 1,
722
745
  },
723
746
  ]}
@@ -849,6 +872,10 @@ export function AudioPlayerTrackItem({
849
872
 
850
873
  const container = resolveContainerSpec(image);
851
874
 
875
+ const handleTrailingButtonPress =
876
+ onTrailingButtonPress ??
877
+ (data.trailingButton === "multiSelect" ? onPress : undefined);
878
+
852
879
  return (
853
880
  <ItemContainer
854
881
  image={image}
@@ -861,22 +888,28 @@ export function AudioPlayerTrackItem({
861
888
  <View style={{ width: container.horizontalGutter }} />
862
889
  <View style={styles.contentColumn}>
863
890
  {showNowPlaying ? (
864
- <>
865
- <NowPlayingRow
866
- assetConfiguration={nowPlayingAsset}
867
- labelConfiguration={nowPlayingLabel}
868
- label={data.nowPlayingLabel}
869
- />
870
- <View style={{ height: container.verticalGutter }} />
871
- </>
891
+ <NowPlayingRow
892
+ assetConfiguration={nowPlayingAsset}
893
+ labelConfiguration={nowPlayingLabel}
894
+ label={data.nowPlayingLabel}
895
+ />
896
+ ) : null}
897
+ {showNowPlaying && (data.textLabel1 || data.textLabel2) ? (
898
+ <View style={{ height: container.verticalGutter }} />
899
+ ) : null}
900
+ {data.textLabel1 ? (
901
+ <ItemLabel
902
+ text={data.textLabel1}
903
+ configuration={textLabel1}
904
+ focused={focused}
905
+ />
906
+ ) : null}
907
+ {data.textLabel1 && data.textLabel2 ? (
908
+ <View style={{ height: container.verticalGutter }} />
909
+ ) : null}
910
+ {data.textLabel2 ? (
911
+ <ItemLabel text={data.textLabel2} configuration={textLabel2} />
872
912
  ) : null}
873
- <ItemLabel
874
- text={data.textLabel1}
875
- configuration={textLabel1}
876
- focused={focused}
877
- />
878
- <View style={{ height: container.verticalGutter }} />
879
- <ItemLabel text={data.textLabel2} configuration={textLabel2} />
880
913
  </View>
881
914
  <ItemButtons
882
915
  leadingButton={data.leadingButton}
@@ -884,7 +917,7 @@ export function AudioPlayerTrackItem({
884
917
  focusedLeadingButton={focusedLeadingButton}
885
918
  focusedTrailingButton={focusedTrailingButton}
886
919
  onLeadingButtonPress={onLeadingButtonPress}
887
- onTrailingButtonPress={onTrailingButtonPress}
920
+ onTrailingButtonPress={handleTrailingButtonPress}
888
921
  isSelected={data.isSelected}
889
922
  renderHandle={renderHandle}
890
923
  checkboxAssets={checkboxAssets}
@@ -905,6 +938,10 @@ export const VARIANT_NOW_PLAYING: ItemConfiguration = {
905
938
  defaultColor: "transparent",
906
939
  focusedColor: "transparent",
907
940
  cornerRadius: 0,
941
+ paddingTop: 8,
942
+ paddingRight: 20,
943
+ paddingBottom: 8,
944
+ paddingLeft: 20,
908
945
  },
909
946
  image: {
910
947
  width: 72,
@@ -925,6 +962,12 @@ export const VARIANT_QUEUED_ITEM: ItemConfiguration = {
925
962
  defaultColor: "transparent",
926
963
  focusedColor: "rgba(62, 62, 62, 1)",
927
964
  cornerRadius: 12,
965
+ paddingTop: 8,
966
+ paddingRight: 10,
967
+ paddingBottom: 8,
968
+ paddingLeft: 10,
969
+ highlightInsetLeft: 10,
970
+ highlightInsetRight: 10,
928
971
  },
929
972
  image: {
930
973
  width: 48,
@@ -945,6 +988,10 @@ export const VARIANT_PLAYLIST_ITEM: ItemConfiguration = {
945
988
  defaultColor: "transparent",
946
989
  focusedColor: "rgba(62, 62, 62, 1)",
947
990
  cornerRadius: 12,
991
+ paddingTop: 8,
992
+ paddingRight: 10,
993
+ paddingBottom: 8,
994
+ paddingLeft: 10,
948
995
  },
949
996
  image: {
950
997
  width: 48,
@@ -1,5 +1,5 @@
1
- import React from "react";
2
- import { StyleSheet, View } from "react-native";
1
+ import React, { useEffect, useState } from "react";
2
+ import { StyleSheet, Text, View } from "react-native";
3
3
  import {
4
4
  AudioPlayerTrackItem,
5
5
  VARIANT_NOW_PLAYING,
@@ -7,8 +7,11 @@ import {
7
7
  } from "./Item";
8
8
  import { AudioPlayerHeader, VARIANT_SECTION_HEADER } from "./Header";
9
9
  import type { MenuItem } from "@applicaster/zapp-react-native-utils/modalState/types";
10
- import { usePlayer } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer";
11
- import { usePlayerState } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerState";
10
+ import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils/playerManager";
11
+ import { PlayerRole } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/const";
12
+ import type { Player } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/player";
13
+
14
+ const DEFAULT_EMPTY_MESSAGE = "Queued items will be added here";
12
15
 
13
16
  const styles = StyleSheet.create({
14
17
  divider: {
@@ -17,27 +20,149 @@ const styles = StyleSheet.create({
17
20
  marginHorizontal: 20,
18
21
  marginVertical: 8,
19
22
  },
23
+ emptyMessage: {
24
+ paddingTop: 32,
25
+ paddingBottom: 32,
26
+ paddingHorizontal: 40,
27
+ textAlign: "center",
28
+ color: "#999999",
29
+ fontSize: 13,
30
+ lineHeight: 16,
31
+ },
20
32
  });
21
33
 
34
+ const isPrimaryOrCastingPlayer = (player?: Player | null): boolean => {
35
+ if (!player) {
36
+ return false;
37
+ }
38
+
39
+ return (
40
+ player.playerRole === PlayerRole.Primary ||
41
+ player.playerRole === PlayerRole.Chromecast
42
+ );
43
+ };
44
+
45
+ const getTargetPlayer = (): Player | null => {
46
+ if (playerManager.isCasting()) {
47
+ return playerManager.getCastingReceiver() || null;
48
+ }
49
+
50
+ const active = playerManager.getActivePlayer();
51
+
52
+ return isPrimaryOrCastingPlayer(active) ? active : null;
53
+ };
54
+
22
55
  type NowPlayingHeaderSectionProps = {
23
56
  onClear?: () => void;
57
+ nextUpTitle?: string;
58
+ nowPlayingTitle?: string;
59
+ isEmpty?: boolean;
60
+ emptyMessage?: string;
61
+ clearLabel?: string;
24
62
  };
25
63
 
26
64
  export function NowPlayingHeaderSection({
27
65
  onClear,
66
+ nextUpTitle = "Next Up",
67
+ nowPlayingTitle = "Now Playing",
68
+ isEmpty = false,
69
+ emptyMessage = DEFAULT_EMPTY_MESSAGE,
70
+ clearLabel,
28
71
  }: NowPlayingHeaderSectionProps) {
29
- const player = usePlayer();
30
- const playerState = usePlayerState("queue-now-playing-section");
72
+ const [activePlayer, setActivePlayer] = useState<Player | null>(
73
+ getTargetPlayer
74
+ );
75
+
76
+ const [currentEntry, setCurrentEntry] = useState<ZappEntry | null>(
77
+ () => getTargetPlayer()?.entry || null
78
+ );
79
+
80
+ const [isPaused, setIsPaused] = useState<boolean>(() =>
81
+ Boolean(getTargetPlayer()?.isPaused())
82
+ );
83
+
84
+ useEffect(() => {
85
+ const handleActivePlayerUpdate = () => {
86
+ const player = getTargetPlayer();
87
+
88
+ if (player) {
89
+ setActivePlayer(player);
90
+
91
+ if (player.entry) {
92
+ setCurrentEntry(player.entry);
93
+ }
94
+
95
+ setIsPaused(Boolean(player.isPaused()));
96
+ }
97
+ };
98
+
99
+ handleActivePlayerUpdate();
100
+
101
+ const removeLifecycle = playerManager.addLifecycleListener({
102
+ onRegistered: (registeredPlayer) => {
103
+ if (isPrimaryOrCastingPlayer(registeredPlayer)) {
104
+ handleActivePlayerUpdate();
105
+ }
106
+ },
107
+ onCastingChanged: handleActivePlayerUpdate,
108
+ });
109
+
110
+ return () => {
111
+ removeLifecycle?.();
112
+ };
113
+ }, []);
114
+
115
+ useEffect(() => {
116
+ if (!activePlayer) {
117
+ return;
118
+ }
119
+
120
+ const syncPlayerState = () => {
121
+ if (activePlayer.entry) {
122
+ setCurrentEntry(activePlayer.entry);
123
+ }
124
+
125
+ setIsPaused(Boolean(activePlayer.isPaused()));
126
+ };
127
+
128
+ syncPlayerState();
129
+
130
+ const removeListener = activePlayer.addListener({
131
+ id: "queue-now-playing-header",
132
+ listener: {
133
+ onPlayerPause: () => setIsPaused(true),
134
+ onPlayerResume: () => setIsPaused(false),
135
+ onVideoLoad: syncPlayerState,
136
+ onLoad: syncPlayerState,
137
+ },
138
+ });
139
+
140
+ return () => {
141
+ removeListener?.();
142
+ };
143
+ }, [activePlayer]);
144
+
145
+ const handlePlayPause = () => {
146
+ if (!activePlayer) {
147
+ return;
148
+ }
149
+
150
+ if (isPaused) {
151
+ activePlayer.play();
152
+ setIsPaused(false);
153
+ } else {
154
+ activePlayer.pause();
155
+ setIsPaused(true);
156
+ }
157
+ };
31
158
 
32
159
  const renderNowPlayingTrack = () => {
33
- if (!player?.entry) {
160
+ if (!currentEntry) {
34
161
  return null;
35
162
  }
36
163
 
37
- const mediaGroup = player.entry.media_group;
38
-
39
- const imageGroup = Array.isArray(mediaGroup)
40
- ? mediaGroup.find((g: any) => g.type === "image")
164
+ const imageGroup = Array.isArray(currentEntry.media_group)
165
+ ? currentEntry.media_group.find((g: any) => g.type === "image")
41
166
  : null;
42
167
 
43
168
  const firstImage = Array.isArray(imageGroup?.media_item)
@@ -45,34 +170,24 @@ export function NowPlayingHeaderSection({
45
170
  : imageGroup?.media_item?.src;
46
171
 
47
172
  const nowPlayingItem: MenuItem = {
48
- id: String(player.entry.id ?? "now_playing"),
49
- title: (player.entry.title as string) || String(player.entry.id),
50
- summary: (player.entry.summary as string) ?? "",
173
+ id: String(currentEntry.id ?? "now_playing"),
174
+ title: (currentEntry.title as string) || String(currentEntry.id),
175
+ summary: (currentEntry.summary as string) ?? "",
51
176
  icon: firstImage,
52
177
  };
53
178
 
54
179
  const data = mapMenuItemToNowPlayingData(
55
180
  nowPlayingItem,
56
- playerState?.isPaused
181
+ isPaused,
182
+ nowPlayingTitle
57
183
  );
58
184
 
59
- const handlePlayPause = () => {
60
- if (playerState?.isPaused) {
61
- player?.play();
62
- } else {
63
- player?.pause();
64
- }
65
- };
66
-
67
185
  return (
68
- <View>
69
- <AudioPlayerTrackItem
70
- configuration={VARIANT_NOW_PLAYING}
71
- data={data}
72
- onTrailingButtonPress={handlePlayPause}
73
- />
74
- <View style={styles.divider} />
75
- </View>
186
+ <AudioPlayerTrackItem
187
+ configuration={VARIANT_NOW_PLAYING}
188
+ data={data}
189
+ onTrailingButtonPress={handlePlayPause}
190
+ />
76
191
  );
77
192
  };
78
193
 
@@ -81,9 +196,10 @@ export function NowPlayingHeaderSection({
81
196
  {renderNowPlayingTrack()}
82
197
  <AudioPlayerHeader
83
198
  configuration={VARIANT_SECTION_HEADER}
84
- data={{ title: "Next Up" }}
199
+ data={{ title: nextUpTitle, clearLabel }}
85
200
  onClear={onClear}
86
201
  />
202
+ {isEmpty ? <Text style={styles.emptyMessage}>{emptyMessage}</Text> : null}
87
203
  </View>
88
204
  );
89
205
  }
@@ -0,0 +1,113 @@
1
+ import React from "react";
2
+ import { Pressable } from "react-native";
3
+ import { fireEvent, render, screen } from "@testing-library/react-native";
4
+ import {
5
+ AudioPlayerTrackItem,
6
+ VARIANT_PLAYLIST_ITEM,
7
+ mapMenuItemToNowPlayingData,
8
+ } from "../Item";
9
+
10
+ const playlistItemData = {
11
+ textLabel1: "Queue",
12
+ textLabel2: "",
13
+ trailingButton: "multiSelect" as const,
14
+ isSelected: false,
15
+ };
16
+
17
+ describe("AudioPlayerTrackItem multiSelect checkbox", () => {
18
+ it("invokes onPress when the checkbox is pressed even if onTrailingButtonPress is omitted", () => {
19
+ const onPress = jest.fn();
20
+
21
+ render(
22
+ <AudioPlayerTrackItem
23
+ configuration={VARIANT_PLAYLIST_ITEM}
24
+ data={playlistItemData}
25
+ onPress={onPress}
26
+ />
27
+ );
28
+
29
+ const pressables = screen.UNSAFE_getAllByType(Pressable);
30
+ fireEvent.press(pressables[pressables.length - 1]);
31
+
32
+ expect(onPress).toHaveBeenCalledTimes(1);
33
+ });
34
+
35
+ it("invokes onTrailingButtonPress instead of onPress when a dedicated trailing handler is provided", () => {
36
+ const onPress = jest.fn();
37
+ const onTrailingButtonPress = jest.fn();
38
+
39
+ render(
40
+ <AudioPlayerTrackItem
41
+ configuration={VARIANT_PLAYLIST_ITEM}
42
+ data={playlistItemData}
43
+ onPress={onPress}
44
+ onTrailingButtonPress={onTrailingButtonPress}
45
+ />
46
+ );
47
+
48
+ const pressables = screen.UNSAFE_getAllByType(Pressable);
49
+ fireEvent.press(pressables[pressables.length - 1]);
50
+
51
+ expect(onTrailingButtonPress).toHaveBeenCalledTimes(1);
52
+ expect(onPress).not.toHaveBeenCalled();
53
+ });
54
+
55
+ it("invokes onPress when the row is pressed", () => {
56
+ const onPress = jest.fn();
57
+
58
+ render(
59
+ <AudioPlayerTrackItem
60
+ configuration={VARIANT_PLAYLIST_ITEM}
61
+ data={playlistItemData}
62
+ onPress={onPress}
63
+ />
64
+ );
65
+
66
+ fireEvent.press(screen.UNSAFE_getAllByType(Pressable)[0]);
67
+
68
+ expect(onPress).toHaveBeenCalledTimes(1);
69
+ });
70
+
71
+ it("renders only textLabel1 Text element when textLabel2 is missing or empty", () => {
72
+ const { UNSAFE_getAllByType, queryByText } = render(
73
+ <AudioPlayerTrackItem
74
+ configuration={VARIANT_PLAYLIST_ITEM}
75
+ data={{
76
+ textLabel1: "Only Title",
77
+ textLabel2: "",
78
+ }}
79
+ />
80
+ );
81
+
82
+ const { Text } = require("react-native");
83
+ const textNodes = UNSAFE_getAllByType(Text);
84
+ expect(textNodes).toHaveLength(1);
85
+ expect(queryByText("Only Title")).toBeTruthy();
86
+ });
87
+ });
88
+
89
+ describe("mapMenuItemToNowPlayingData", () => {
90
+ const item = {
91
+ id: "item_1",
92
+ title: "Track Title",
93
+ summary: "Artist Name",
94
+ icon: "https://example.com/cover.png",
95
+ };
96
+
97
+ it("uses default 'Now Playing' label when omitted", () => {
98
+ const data = mapMenuItemToNowPlayingData(item as any);
99
+ expect(data.nowPlayingLabel).toBe("Now Playing");
100
+ expect(data.textLabel1).toBe("Track Title");
101
+ expect(data.textLabel2).toBe("Artist Name");
102
+ });
103
+
104
+ it("uses custom nowPlayingLabel when provided", () => {
105
+ const data = mapMenuItemToNowPlayingData(
106
+ item as any,
107
+ false,
108
+ "En cours de lecture"
109
+ );
110
+
111
+ expect(data.nowPlayingLabel).toBe("En cours de lecture");
112
+ });
113
+ });
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+ import { render, screen } from "@testing-library/react-native";
3
+ import { NowPlayingHeaderSection } from "../NowPlayingHeaderSection";
4
+
5
+ jest.mock(
6
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayer",
7
+ () => ({
8
+ usePlayer: () => null,
9
+ })
10
+ );
11
+
12
+ jest.mock(
13
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager/usePlayerState",
14
+ () => ({
15
+ usePlayerState: () => ({ isPaused: true }),
16
+ })
17
+ );
18
+
19
+ const EMPTY_COPY = "Queued items will be added here";
20
+
21
+ describe("NowPlayingHeaderSection empty queue", () => {
22
+ it("renders the empty message under Next Up when the list is empty", () => {
23
+ render(<NowPlayingHeaderSection isEmpty emptyMessage={EMPTY_COPY} />);
24
+
25
+ expect(screen.getByText("Next Up")).toBeTruthy();
26
+ expect(screen.getByText(EMPTY_COPY)).toBeTruthy();
27
+ });
28
+
29
+ it("does not render the empty message when the list has items", () => {
30
+ render(
31
+ <NowPlayingHeaderSection isEmpty={false} emptyMessage={EMPTY_COPY} />
32
+ );
33
+
34
+ expect(screen.queryByText(EMPTY_COPY)).toBeNull();
35
+ });
36
+ });
@@ -1,5 +1,3 @@
1
- export { AudioPlayerAction } from "./Action";
2
-
3
1
  export { AudioPlayerButton, VARIANT_WITH_ICON } from "./Button";
4
2
 
5
3
  export { AudioPlayerHeader } from "./Header";