@applicaster/zapp-react-native-ui-components 16.0.0-alpha.3112220329 → 16.0.0-alpha.3202041045

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 (76) hide show
  1. package/Components/LinkHandler/LinkHandler.tsx +2 -7
  2. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +17 -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 +14 -6
  7. package/Components/MasterCell/DefaultComponents/PressableView.tsx +3 -0
  8. package/Components/MasterCell/DefaultComponents/__tests__/ActionButton.test.tsx +288 -0
  9. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +2 -1
  10. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +4 -2
  11. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/factory.test.ts +32 -0
  12. package/Components/MasterCell/__tests__/masterCell.test.js +60 -0
  13. package/Components/MasterCell/index.tsx +28 -4
  14. package/Components/MasterCell/utils/__tests__/withoutFocusedState.test.js +20 -0
  15. package/Components/MasterCell/utils/index.ts +18 -0
  16. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +4 -2
  17. package/Components/ModalComponent/ActionsBottomSheet/__tests__/openActionsBottomSheet.test.ts +38 -0
  18. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +5 -2
  19. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +4 -1
  20. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +58 -73
  21. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +65 -56
  22. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +140 -65
  23. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +115 -47
  24. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +148 -32
  25. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +164 -0
  26. package/Components/ModalComponent/AudioPlayer/Components/__tests__/NowPlayingHeaderSection.test.tsx +36 -0
  27. package/Components/ModalComponent/AudioPlayer/Components/index.ts +0 -2
  28. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +209 -6
  29. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +250 -19
  30. package/Components/ModalComponent/BottomSheetModalContent.tsx +126 -77
  31. package/Components/ModalComponent/SortableList.tsx +36 -22
  32. package/Components/ModalComponent/TextInputContent.tsx +42 -7
  33. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +111 -6
  34. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +50 -1
  35. package/Components/ModalComponent/__tests__/showTextInput.test.ts +25 -0
  36. package/Components/ModalComponent/index.tsx +7 -9
  37. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +24 -0
  38. package/Components/ModalComponent/presets/selectableCell.tsx +1 -1
  39. package/Components/ModalComponent/showTextInput.ts +13 -2
  40. package/Components/OfflineHandler/__tests__/index.test.tsx +109 -0
  41. package/Components/OfflineHandler/index.tsx +42 -4
  42. package/Components/PlayerContainer/PlayerContainer.tsx +30 -6
  43. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +51 -0
  44. package/Components/River/ComponentsMap/ComponentsMap.tsx +14 -13
  45. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +140 -75
  46. package/Components/River/__tests__/componentsMap.test.js +8 -3
  47. package/Components/Screen/__tests__/Screen.test.tsx +24 -0
  48. package/Components/Screen/__tests__/__snapshots__/Screen.test.tsx.snap +2 -0
  49. package/Components/Screen/index.tsx +5 -0
  50. package/Components/ScreenRevealManager/withScreenRevealManager.tsx +1 -1
  51. package/Components/Transitioner/AnimationManager.js +1 -1
  52. package/Components/Transitioner/Scene.tsx +4 -3
  53. package/Components/Transitioner/Transitioner.tsx +71 -33
  54. package/Components/Transitioner/__tests__/transitioner.test.js +372 -3
  55. package/Components/VideoLive/LiveImageManager.ts +134 -14
  56. package/Components/VideoLive/MuteUnmuteController.ts +135 -0
  57. package/Components/VideoLive/PlayerLiveImageComponent.tsx +15 -19
  58. package/Components/VideoLive/__tests__/LiveImageManager.test.ts +59 -0
  59. package/Components/VideoLive/__tests__/LiveImageMuteState.test.ts +311 -0
  60. package/Components/VideoLive/__tests__/MuteUnmuteController.test.ts +135 -0
  61. package/Components/VideoLive/__tests__/PlayerLiveImageComponent.test.tsx +63 -1
  62. package/Components/VideoModal/PlayerWrapper.tsx +4 -1
  63. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -0
  64. package/Components/VideoModal/__tests__/playerWrapperStyle.test.ts +58 -0
  65. package/Components/VideoModal/__tests__/playerWrapperUtils.test.ts +16 -0
  66. package/Components/VideoModal/playerWrapperStyle.ts +21 -3
  67. package/Components/VideoModal/playerWrapperUtils.ts +7 -1
  68. package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
  69. package/Contexts/ModalNavigationContext/index.tsx +2 -2
  70. package/Decorators/ZappPipesDataConnector/__tests__/screenToolbox.test.ts +347 -0
  71. package/Decorators/ZappPipesDataConnector/__tests__/screenToolboxUnavailable.test.ts +53 -0
  72. package/Decorators/ZappPipesDataConnector/index.tsx +19 -4
  73. package/Decorators/ZappPipesDataConnector/screenToolbox.ts +384 -0
  74. package/assets/empty-Info.png +0 -0
  75. package/package.json +5 -5
  76. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +0 -314
@@ -1,8 +1,7 @@
1
1
  import * as React from "react";
2
2
  import * as R from "ramda";
3
3
 
4
- import { findPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
5
- import { usePlugins } from "@applicaster/zapp-react-native-redux/hooks";
4
+ import { getPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
6
5
  import {
7
6
  inflateString,
8
7
  objectToReadableString,
@@ -40,12 +39,8 @@ export async function inflateUrl(url) {
40
39
 
41
40
  export function LinkHandler(props: Props) {
42
41
  const screenData = props?.screenData;
43
- const plugins = usePlugins();
44
42
 
45
- const ScreenPlugin = findPluginByIdentifier(
46
- WEBVIEW_SCREEN_IDENTIFIER,
47
- plugins
48
- );
43
+ const ScreenPlugin = getPluginByIdentifier(WEBVIEW_SCREEN_IDENTIFIER);
49
44
 
50
45
  const Component =
51
46
  R.path(["module", "Component"], ScreenPlugin) ||
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect } from "react";
2
2
  import { TouchableOpacity, ViewStyle } from "react-native";
3
3
 
4
4
  import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
5
+ import { useUIComponentContext } from "@applicaster/zapp-react-native-ui-components/Contexts/UIComponentContext";
5
6
 
6
7
  import Image from "./Image";
7
8
  type Props = {
@@ -57,6 +58,7 @@ export const ActionButton = React.memo(function ActionButtonComponent(
57
58
  ) {
58
59
  const { item, action, asset, flavour = "flavour_1", cellUUID } = props;
59
60
  const actionContext = useActions(action?.identifier);
61
+ const component = useUIComponentContext();
60
62
 
61
63
  // TODO: add subscription API for action availability
62
64
  const actionDisabled =
@@ -81,8 +83,9 @@ export const ActionButton = React.memo(function ActionButtonComponent(
81
83
  updateState: (state) => {
82
84
  setActionState(state);
83
85
  },
86
+ ...(component ? { component } : {}),
84
87
  });
85
- }, [actionState, actionContext?.state, item?.id]);
88
+ }, [actionState, actionContext?.state, item, component]);
86
89
 
87
90
  useEffect(() => {
88
91
  if (typeof actionContext?.addListener === "function") {
@@ -93,6 +96,14 @@ export const ActionButton = React.memo(function ActionButtonComponent(
93
96
  }, [item?.id, setActionState, actionContext]);
94
97
 
95
98
  if (actionDisabled || !actionContext) return null;
99
+
100
+ if (
101
+ actionState?.visibleInCells &&
102
+ !actionState.visibleInCells.includes(cellUUID)
103
+ ) {
104
+ return null;
105
+ }
106
+
96
107
  const AssetComponent = actionState?.asset;
97
108
 
98
109
  // Default state is not yet ready
@@ -106,6 +117,11 @@ export const ActionButton = React.memo(function ActionButtonComponent(
106
117
  accessibilityLabel={props?.accessibilityLabel || `${item?.id}`}
107
118
  accessibilityHint={props?.accessibilityHint}
108
119
  accessibilityRole="button"
120
+ accessibilityState={
121
+ typeof actionState.checked === "boolean"
122
+ ? { checked: actionState.checked }
123
+ : undefined
124
+ }
109
125
  accessible={!!(props?.testID || props?.accessibilityLabel)}
110
126
  style={props?.style}
111
127
  >
@@ -6,6 +6,7 @@ import React, {
6
6
  useState,
7
7
  } from "react";
8
8
  import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
9
+ import { useUIComponentContext } from "@applicaster/zapp-react-native-ui-components/Contexts/UIComponentContext";
9
10
 
10
11
  type ActionDefinition = {
11
12
  identifier?: string;
@@ -61,6 +62,7 @@ export function ActionButtonController({
61
62
  const resolvedEntry = entry;
62
63
  const resolvedIdentifier = action?.identifier || pluginIdentifier || "";
63
64
  const actionContext = useActions(resolvedIdentifier);
65
+ const component = useUIComponentContext();
64
66
 
65
67
  const actionDisabled =
66
68
  typeof actionContext?.isActionAvailable === "function" &&
@@ -128,20 +130,33 @@ export function ActionButtonController({
128
130
  return;
129
131
  }
130
132
 
133
+ const invokeOptions: InvokeArgsOptions = {
134
+ ...(component ? { component } : {}),
135
+ };
136
+
131
137
  if (supportsEntryState) {
132
138
  return actionContext.invokeAction?.(resolvedEntry, {
139
+ ...invokeOptions,
133
140
  updateState: setActionState,
134
141
  });
135
142
  }
136
143
 
144
+ if (typeof actionContext.invokeAction === "function") {
145
+ return actionContext.invokeAction(resolvedEntry, invokeOptions);
146
+ }
147
+
137
148
  const favouritesAction = legacySelected
138
149
  ? actionContext.removeFavourite
139
150
  : actionContext.addFavourite;
140
151
 
141
- const toggleAction = actionContext?.invokeAction ?? favouritesAction;
142
-
143
- return toggleAction?.(resolvedEntry);
144
- }, [actionContext, supportsEntryState, resolvedEntry, legacySelected]);
152
+ return favouritesAction?.(resolvedEntry);
153
+ }, [
154
+ actionContext,
155
+ supportsEntryState,
156
+ resolvedEntry,
157
+ legacySelected,
158
+ component,
159
+ ]);
145
160
 
146
161
  if (!actionContext || actionDisabled) {
147
162
  if (!actionContext) {
@@ -2,6 +2,7 @@ import React from "react";
2
2
  import { Text } from "react-native";
3
3
  import { render, fireEvent } from "@testing-library/react-native";
4
4
  import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
5
+ import { UIComponentContext } from "@applicaster/zapp-react-native-ui-components/Contexts/UIComponentContext";
5
6
 
6
7
  import { ActionButtonController } from "../ActionButtonController";
7
8
 
@@ -89,6 +90,47 @@ describe("ActionButtonController", () => {
89
90
  );
90
91
  });
91
92
 
93
+ it("passes the List UIComponentContext into invokeAction so the sheet can refresh it", () => {
94
+ const listComponent = {
95
+ id: "list-1",
96
+ data: { source: "https://example.com/user/collections/abc" },
97
+ };
98
+
99
+ const actionContext = {
100
+ isActionAvailable: jest.fn(() => true),
101
+ initialEntryState: jest.fn(() => ({ active: false })),
102
+ invokeAction: jest.fn(),
103
+ addListener: jest.fn(() => jest.fn()),
104
+ };
105
+
106
+ mockUseActions.mockReturnValue(actionContext);
107
+
108
+ const { getByTestId } = render(
109
+ <UIComponentContext.Provider value={listComponent}>
110
+ <ActionButtonController
111
+ action={{ identifier: "open-modal-bottom-sheet-cell-action" }}
112
+ entry={entry}
113
+ >
114
+ {({ onPress }) => (
115
+ <Text testID="content" onPress={onPress}>
116
+ press
117
+ </Text>
118
+ )}
119
+ </ActionButtonController>
120
+ </UIComponentContext.Provider>
121
+ );
122
+
123
+ fireEvent.press(getByTestId("content"));
124
+
125
+ expect(actionContext.invokeAction).toHaveBeenCalledWith(
126
+ entry,
127
+ expect.objectContaining({
128
+ updateState: expect.any(Function),
129
+ component: listComponent,
130
+ })
131
+ );
132
+ });
133
+
92
134
  it("updates state from addListener", () => {
93
135
  let listener;
94
136
 
@@ -328,7 +370,7 @@ describe("ActionButtonController", () => {
328
370
 
329
371
  fireEvent.press(getByTestId("content"));
330
372
 
331
- expect(invokeAction).toHaveBeenCalledWith(entry);
373
+ expect(invokeAction).toHaveBeenCalledWith(entry, {});
332
374
  expect(addFavourite).not.toHaveBeenCalled();
333
375
  expect(removeFavourite).not.toHaveBeenCalled();
334
376
  });
@@ -1,5 +1,4 @@
1
- import { findPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
2
- import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
1
+ import { getPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
3
2
  import { loggerLiveImageManager } from "../../../VideoLive/loggerHelper";
4
3
 
5
4
  const { log_debug, log_error } = loggerLiveImageManager;
@@ -74,10 +73,9 @@ export async function executePreloadHooks({
74
73
  let payload: any = entry;
75
74
 
76
75
  let needsAbort = false;
77
- const plugins = appStore.get("plugins");
78
76
 
79
77
  for (const hookConfig of sortedHooks) {
80
- const plugin = findPluginByIdentifier(hookConfig.identifier, plugins, true);
78
+ const plugin = getPluginByIdentifier(hookConfig.identifier, true);
81
79
 
82
80
  if (!plugin?.module) {
83
81
  log_debug(
@@ -8,7 +8,7 @@ import Image from "../Image";
8
8
  import { useTrackCurrentAutoScrollingElement } from "@applicaster/zapp-react-native-utils/reactHooks/autoscrolling/useTrackCurrentAutoScrollingElement";
9
9
  import { useUIComponentContext } from "@applicaster/zapp-react-native-ui-components/Contexts/UIComponentContext";
10
10
  import { getPropComponentType } from "@applicaster/zapp-react-native-utils/cellUtils";
11
- import { findPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
11
+ import { getPluginByIdentifier } from "@applicaster/zapp-react-native-utils/pluginUtils";
12
12
  import { useAccessibilityState } from "@applicaster/zapp-react-native-utils/appUtils/accessibilityManager/hooks";
13
13
 
14
14
  type LiveImageProps = {
@@ -245,13 +245,21 @@ const LiveImageComponent = (props: LiveImageProps) => {
245
245
 
246
246
  const isCurrentlyFocused = useTrackCurrentAutoScrollingElement(componentId);
247
247
 
248
- const { playerPluginId, screenConfig, preloadHooks } =
249
- getPlayerConfig(player_screen_id, actionIdentifier) ?? {};
248
+ // New object identities here restart PlayerLiveImage's register effect,
249
+ // which throws away the in-flight player and runs preload hooks again.
250
+ const playerConfig = React.useMemo(
251
+ () => getPlayerConfig(player_screen_id, actionIdentifier),
252
+ [player_screen_id, actionIdentifier]
253
+ );
254
+
255
+ const { playerPluginId, screenConfig, preloadHooks } = playerConfig ?? {};
250
256
 
251
- const playableEntry = playerPluginId ? getPlayableEntry(item) : null;
252
- const plugins = appStore.get("plugins");
257
+ const playableEntry = React.useMemo(
258
+ () => (playerPluginId ? getPlayableEntry(item) : null),
259
+ [playerPluginId, item?.id]
260
+ );
253
261
 
254
- const playerPlugin = findPluginByIdentifier(playerPluginId, plugins);
262
+ const playerPlugin = getPluginByIdentifier(playerPluginId);
255
263
 
256
264
  const isShouldRender =
257
265
  playerPlugin &&
@@ -11,6 +11,7 @@ type Props = {
11
11
  accessibilityHint?: string;
12
12
  onPress?: () => void;
13
13
  testOnly_pressed?: boolean;
14
+ checked?: boolean;
14
15
  };
15
16
 
16
17
  export function PressableView({
@@ -22,11 +23,13 @@ export function PressableView({
22
23
  pressedStyle,
23
24
  onPress,
24
25
  testOnly_pressed = false,
26
+ checked,
25
27
  }: Props) {
26
28
  return (
27
29
  <Pressable
28
30
  testOnly_pressed={testOnly_pressed}
29
31
  onPress={onPress}
32
+ accessibilityState={checked === undefined ? undefined : { checked }}
30
33
  testID={testID}
31
34
  accessibilityLabel={accessibilityLabel}
32
35
  accessibilityHint={accessibilityHint}
@@ -0,0 +1,288 @@
1
+ import React from "react";
2
+ import { act, fireEvent, render } from "@testing-library/react-native";
3
+
4
+ import { ActionButton } from "../ActionButton";
5
+ import { ActionButton as MobileActionButton } from "../mobile/MobileActionButtons/ActionButton";
6
+ import { Text } from "react-native";
7
+
8
+ type EntryStateListener = (state: CellActionEntryState) => void;
9
+
10
+ const PLUGIN_ASSETS = {
11
+ mute: ["plugin_mute_1", "plugin_mute_2"],
12
+ unmute: ["plugin_unmute_1", "plugin_unmute_2"],
13
+ };
14
+
15
+ // Fake of the mute_unmute_action context, following its entry state contract.
16
+ type FakeState = {
17
+ available: boolean;
18
+ muted: boolean;
19
+ visibleInCells?: string[];
20
+ };
21
+
22
+ const createFakeContext = (initial: FakeState) => {
23
+ let current = initial;
24
+ const listeners = new Set<EntryStateListener>();
25
+
26
+ const toEntryState = ({
27
+ available,
28
+ muted,
29
+ visibleInCells,
30
+ }: FakeState): CellActionEntryState => ({
31
+ state: muted ? 1 : 0,
32
+ asset: available
33
+ ? muted
34
+ ? PLUGIN_ASSETS.unmute
35
+ : PLUGIN_ASSETS.mute
36
+ : undefined,
37
+ label: muted ? "Unmute" : "Mute",
38
+ mobileButtonAssets: muted ? PLUGIN_ASSETS.unmute : PLUGIN_ASSETS.mute,
39
+ visibleInCells,
40
+ });
41
+
42
+ const emit = (next: FakeState) => {
43
+ current = next;
44
+ listeners.forEach((listener) => listener(toEntryState(current)));
45
+ };
46
+
47
+ const context = {
48
+ initialEntryState: jest.fn(() => toEntryState(current)),
49
+ addListener: jest.fn((_id: string, listener: EntryStateListener) => {
50
+ listeners.add(listener);
51
+
52
+ return () => listeners.delete(listener);
53
+ }),
54
+ invokeAction: jest.fn((_item, options) => {
55
+ if (!current.available) return;
56
+
57
+ current = { ...current, muted: !current.muted };
58
+ options?.updateState?.(toEntryState(current));
59
+ listeners.forEach((listener) => listener(toEntryState(current)));
60
+ }),
61
+ };
62
+
63
+ return { context, emit, listeners };
64
+ };
65
+
66
+ let mockContext: ReturnType<typeof createFakeContext>["context"];
67
+
68
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/actions", () => ({
69
+ useActions: jest.fn(() => mockContext),
70
+ }));
71
+
72
+ jest.mock(
73
+ "@applicaster/zapp-react-native-ui-components/Contexts/UIComponentContext",
74
+ () => ({ useUIComponentContext: jest.fn(() => undefined) })
75
+ );
76
+
77
+ jest.mock("../Image", () => {
78
+ const { Text: MockText } = require("react-native");
79
+
80
+ return {
81
+ __esModule: true,
82
+ default: ({ uri }) => <MockText testID="asset">{uri}</MockText>,
83
+ };
84
+ });
85
+
86
+ const item = { id: "entry-1" } as unknown as ZappEntry;
87
+
88
+ it("keeps the standalone button visible and limits actions to the playing cell instance", () => {
89
+ const fake = createFakeContext({
90
+ available: true,
91
+ muted: true,
92
+ visibleInCells: [],
93
+ });
94
+
95
+ mockContext = fake.context;
96
+
97
+ const Label = ({
98
+ children,
99
+ testID,
100
+ }: {
101
+ children?: React.ReactNode;
102
+ testID?: string;
103
+ mobileActionRole?: string;
104
+ }) => <Text testID={testID}>{children}</Text>;
105
+
106
+ const { getByTestId, queryByTestId } = render(
107
+ <>
108
+ {["first", "second"].map((cellUUID) => (
109
+ <ActionButton
110
+ key={cellUUID}
111
+ item={item}
112
+ action={{ identifier: "mute_unmute_action" }}
113
+ cellUUID={cellUUID}
114
+ testID={`cell-${cellUUID}`}
115
+ style={{}}
116
+ />
117
+ ))}
118
+ <MobileActionButton
119
+ action={{ identifier: "mute_unmute_action" }}
120
+ entry={item}
121
+ testID="standalone"
122
+ style={{}}
123
+ focusedStyles={{}}
124
+ >
125
+ <Label mobileActionRole="label">Toggle sound</Label>
126
+ </MobileActionButton>
127
+ </>
128
+ );
129
+
130
+ expect(queryByTestId("cell-first")).toBeNull();
131
+ expect(queryByTestId("cell-second")).toBeNull();
132
+ expect(getByTestId("standalone")).toBeTruthy();
133
+ fireEvent.press(getByTestId("standalone"));
134
+ expect(fake.context.invokeAction).toHaveBeenCalledTimes(1);
135
+
136
+ act(() =>
137
+ fake.emit({ available: true, muted: false, visibleInCells: ["first"] })
138
+ );
139
+
140
+ expect(getByTestId("cell-first")).toBeTruthy();
141
+ expect(queryByTestId("cell-second")).toBeNull();
142
+
143
+ act(() =>
144
+ fake.emit({ available: true, muted: false, visibleInCells: ["second"] })
145
+ );
146
+
147
+ expect(queryByTestId("cell-first")).toBeNull();
148
+ expect(getByTestId("cell-second")).toBeTruthy();
149
+
150
+ act(() => fake.emit({ available: true, muted: false, visibleInCells: [] }));
151
+
152
+ expect(queryByTestId("cell-first")).toBeNull();
153
+ expect(queryByTestId("cell-second")).toBeNull();
154
+ expect(getByTestId("standalone")).toBeTruthy();
155
+ });
156
+
157
+ const renderButton = (
158
+ flavour: "flavour_1" | "flavour_2",
159
+ asset?: React.ComponentProps<typeof ActionButton>["asset"]
160
+ ) =>
161
+ render(
162
+ <ActionButton
163
+ item={item}
164
+ flavour={flavour}
165
+ action={{ identifier: "mute_unmute_action" }}
166
+ asset={asset}
167
+ style={{}}
168
+ testID="mute-button"
169
+ />
170
+ );
171
+
172
+ const cellAsset = {
173
+ props: {},
174
+ src: { active: "cell_selected", inactive: "cell_unselected" },
175
+ style: {},
176
+ type: "Image",
177
+ } as const;
178
+
179
+ describe.each([
180
+ ["flavour_1", 0],
181
+ ["flavour_2", 1],
182
+ ] as const)("mute_unmute_action cell action (%s)", (flavour, index) => {
183
+ let fake: ReturnType<typeof createFakeContext>;
184
+
185
+ const setup = (state: { available: boolean; muted: boolean }, asset?) => {
186
+ fake = createFakeContext(state);
187
+ mockContext = fake.context;
188
+
189
+ return renderButton(flavour, asset);
190
+ };
191
+
192
+ it("renders nothing while the preview has not started", () => {
193
+ const { queryByTestId } = setup({ available: false, muted: false });
194
+
195
+ expect(queryByTestId("mute-button")).toBeNull();
196
+ expect(queryByTestId("asset")).toBeNull();
197
+ });
198
+
199
+ it("shows the Mute icon while the preview plays with sound on", () => {
200
+ const { getByTestId } = setup({ available: true, muted: false });
201
+
202
+ expect(getByTestId("asset").props.children).toBe(PLUGIN_ASSETS.mute[index]);
203
+ });
204
+
205
+ it("shows the Unmute icon while the preview plays muted", () => {
206
+ const { getByTestId } = setup({ available: true, muted: true });
207
+
208
+ expect(getByTestId("asset").props.children).toBe(
209
+ PLUGIN_ASSETS.unmute[index]
210
+ );
211
+ });
212
+
213
+ it("toggles the sound and updates the icon on tap", () => {
214
+ const { getByTestId } = setup({ available: true, muted: false });
215
+
216
+ fireEvent.press(getByTestId("mute-button"));
217
+
218
+ expect(fake.context.invokeAction).toHaveBeenCalledTimes(1);
219
+
220
+ expect(getByTestId("asset").props.children).toBe(
221
+ PLUGIN_ASSETS.unmute[index]
222
+ );
223
+
224
+ fireEvent.press(getByTestId("mute-button"));
225
+
226
+ expect(getByTestId("asset").props.children).toBe(PLUGIN_ASSETS.mute[index]);
227
+ });
228
+
229
+ it("follows state emitted through addListener", () => {
230
+ const { getByTestId, queryByTestId } = setup({
231
+ available: false,
232
+ muted: false,
233
+ });
234
+
235
+ act(() => fake.emit({ available: true, muted: false }));
236
+
237
+ expect(getByTestId("asset").props.children).toBe(PLUGIN_ASSETS.mute[index]);
238
+
239
+ act(() => fake.emit({ available: true, muted: true }));
240
+
241
+ expect(getByTestId("asset").props.children).toBe(
242
+ PLUGIN_ASSETS.unmute[index]
243
+ );
244
+
245
+ expect(queryByTestId("mute-button")).not.toBeNull();
246
+ });
247
+
248
+ it("hides again when the preview stops or another cell starts", () => {
249
+ const { queryByTestId } = setup({ available: true, muted: false });
250
+
251
+ expect(queryByTestId("mute-button")).not.toBeNull();
252
+
253
+ act(() => fake.emit({ available: false, muted: false }));
254
+
255
+ expect(queryByTestId("mute-button")).toBeNull();
256
+ });
257
+
258
+ it("unsubscribes from the context on unmount", () => {
259
+ const { unmount } = setup({ available: true, muted: false });
260
+
261
+ expect(fake.listeners.size).toBe(1);
262
+
263
+ unmount();
264
+
265
+ expect(fake.listeners.size).toBe(0);
266
+ });
267
+
268
+ it("prefers the cell's selected icon when muted (state 1)", () => {
269
+ const { getByTestId } = setup({ available: true, muted: true }, cellAsset);
270
+
271
+ expect(getByTestId("asset").props.children).toBe("cell_selected");
272
+ });
273
+
274
+ it("prefers the cell's unselected icon when sound is on (state 0)", () => {
275
+ const { getByTestId } = setup({ available: true, muted: false }, cellAsset);
276
+
277
+ expect(getByTestId("asset").props.children).toBe("cell_unselected");
278
+ });
279
+
280
+ it("uses the plugin asset when the cell has no icons", () => {
281
+ const { getByTestId } = setup({ available: true, muted: false }, {
282
+ ...cellAsset,
283
+ src: { active: undefined, inactive: undefined },
284
+ } as any);
285
+
286
+ expect(getByTestId("asset").props.children).toBe(PLUGIN_ASSETS.mute[index]);
287
+ });
288
+ });
@@ -166,9 +166,10 @@ export const ActionButton = ({
166
166
  <PressableView
167
167
  testOnly_pressed={props.testOnly_pressed}
168
168
  testID={props.testID || `${entry?.id}`}
169
- style={style}
169
+ style={[style, props.alignment]}
170
170
  pressedStyle={focusedStyles}
171
171
  onPress={renderProps.onPress}
172
+ checked={renderProps.actionState?.checked}
172
173
  accessibilityLabel={props.accessibilityLabel || `${entry?.id}`}
173
174
  accessibilityHint={props.accessibilityHint}
174
175
  >
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import { Pressable, Text as RNText, View } from "react-native";
2
+ import { Pressable, StyleSheet, Text as RNText, View } from "react-native";
3
3
  import { render, fireEvent } from "@testing-library/react-native";
4
4
  import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
5
5
 
@@ -282,7 +282,9 @@ describe("MobileActionButton", () => {
282
282
  expect(pressable.props.testID).toBe("mobile_action_button_1");
283
283
  expect(pressable.props.accessibilityLabel).toBe("entry-1");
284
284
 
285
- expect(pressable.props.style({ pressed: false })[0]).toMatchObject({
285
+ expect(
286
+ StyleSheet.flatten(pressable.props.style({ pressed: false }))
287
+ ).toMatchObject({
286
288
  backgroundColor: "rgba(1,1,1,1)",
287
289
  borderColor: "rgba(2,2,2,1)",
288
290
  borderWidth: 1,
@@ -0,0 +1,32 @@
1
+ import { CollapsibleTextContainer } from "../index";
2
+
3
+ const getValue = (key: string) => {
4
+ const values: Record<string, unknown> = {
5
+ text_alignment: "left",
6
+ margin_top: 0,
7
+ margin_right: 0,
8
+ margin_bottom: 0,
9
+ margin_left: 0,
10
+ padding_top: 0,
11
+ padding_right: 0,
12
+ padding_bottom: 0,
13
+ padding_left: 0,
14
+ corner_radius: 0,
15
+ data_key: "title",
16
+ custom_data_key: undefined,
17
+ };
18
+
19
+ return values[key];
20
+ };
21
+
22
+ describe("CollapsibleTextContainer view tree", () => {
23
+ it("aligns the label to the text alignment instead of stretching the cell", () => {
24
+ const result = CollapsibleTextContainer({
25
+ getValue,
26
+ elements: [],
27
+ additionalStyles: { backgroundColor: "transparent" },
28
+ });
29
+
30
+ expect(result.style.alignSelf).toBe("flex-start");
31
+ });
32
+ });