@applicaster/zapp-react-native-utils 16.0.0-rc.12 → 16.0.0-rc.120

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 (224) hide show
  1. package/actionUtils/index.ts +3 -1
  2. package/actionsExecutor/ActionExecutor.ts +57 -12
  3. package/actionsExecutor/ActionExecutorContext.tsx +113 -314
  4. package/actionsExecutor/__tests__/ActionExecutor.test.ts +71 -0
  5. package/actionsExecutor/__tests__/feedDecorator.test.ts +61 -0
  6. package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +25 -0
  7. package/actionsExecutor/actions/__tests__/goBack.test.ts +62 -0
  8. package/actionsExecutor/actions/__tests__/goHome.test.ts +19 -0
  9. package/actionsExecutor/actions/__tests__/loadItemsFromSource.test.ts +88 -0
  10. package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +267 -0
  11. package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +100 -0
  12. package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +300 -0
  13. package/actionsExecutor/actions/__tests__/presentScreen.test.ts +127 -0
  14. package/actionsExecutor/actions/__tests__/showAlert.test.ts +49 -0
  15. package/actionsExecutor/actions/__tests__/showToast.test.ts +88 -0
  16. package/actionsExecutor/actions/appRestart.ts +24 -0
  17. package/actionsExecutor/actions/confirmDialog.ts +53 -0
  18. package/actionsExecutor/actions/dismissBottomSheet.ts +23 -0
  19. package/actionsExecutor/actions/goBack.ts +60 -0
  20. package/actionsExecutor/actions/goHome.ts +31 -0
  21. package/actionsExecutor/actions/index.ts +42 -0
  22. package/actionsExecutor/actions/localStorageRemove.ts +27 -0
  23. package/actionsExecutor/actions/localStorageSet.ts +28 -0
  24. package/actionsExecutor/actions/localStorageToggleFlag.ts +28 -0
  25. package/actionsExecutor/actions/navigateToScreen.ts +121 -0
  26. package/actionsExecutor/actions/openBottomSheet.ts +339 -0
  27. package/actionsExecutor/actions/presentScreen.ts +78 -0
  28. package/actionsExecutor/actions/refreshComponent.ts +85 -0
  29. package/actionsExecutor/actions/screenSetVariable.ts +35 -0
  30. package/actionsExecutor/actions/screenToggleFlag.ts +38 -0
  31. package/actionsExecutor/actions/sendCloudEvent.ts +93 -0
  32. package/actionsExecutor/actions/sessionStorageRemove.ts +19 -0
  33. package/actionsExecutor/actions/sessionStorageSet.ts +20 -0
  34. package/actionsExecutor/actions/sessionStorageToggleFlag.ts +15 -0
  35. package/actionsExecutor/actions/showAlert.ts +51 -0
  36. package/actionsExecutor/actions/showToast.ts +87 -0
  37. package/actionsExecutor/actions/switchLayout.ts +40 -0
  38. package/actionsExecutor/consts.ts +27 -0
  39. package/actionsExecutor/feedDecorator.ts +6 -6
  40. package/actionsExecutor/types.ts +59 -0
  41. package/analyticsUtils/AnalyticsEvents/sendMenuClickEvent.ts +2 -2
  42. package/analyticsUtils/PlayerAnalyticsManager.ts +12 -2
  43. package/analyticsUtils/__tests__/analyticsMapper.test.ts +35 -0
  44. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testACP_events.json +1 -1
  45. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testBlockUnlistedParams_rules.json +1 -1
  46. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_events.json +4 -0
  47. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_rules.json +6 -0
  48. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEventRegex_events.json +3 -9
  49. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_events.json +18 -0
  50. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_rules.json +16 -0
  51. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_events.json +4 -0
  52. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_rules.json +6 -0
  53. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_events.json +4 -0
  54. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_rules.json +6 -0
  55. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_events.json +4 -0
  56. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_rules.json +6 -0
  57. package/analyticsUtils/__tests__/fixtures/index.js +20 -0
  58. package/analyticsUtils/analyticsMapper.ts +4 -1
  59. package/analyticsUtils/playerAnalyticsTracker.ts +26 -3
  60. package/appUtils/HooksManager/index.ts +9 -0
  61. package/appUtils/contextKeysManager/__tests__/getKey/failure.test.ts +1 -1
  62. package/appUtils/contextKeysManager/__tests__/removeKey/failure.test.ts +1 -1
  63. package/appUtils/contextKeysManager/__tests__/setKey/failure/invalidKey.test.ts +4 -4
  64. package/appUtils/contextKeysManager/index.ts +1 -1
  65. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +142 -59
  66. package/appUtils/playerManager/OverlayObserver/__tests__/getUpcomingQueue.test.ts +67 -0
  67. package/appUtils/playerManager/OverlayObserver/__tests__/liveContent.test.ts +205 -0
  68. package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +56 -0
  69. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.android.tv.ts +4 -0
  70. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ios.tv.ts +4 -0
  71. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +24 -0
  72. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.web.ts +4 -0
  73. package/appUtils/playerManager/OverlayObserver/utils.ts +51 -20
  74. package/appUtils/playerManager/__tests__/playerContent.test.ts +403 -0
  75. package/appUtils/playerManager/__tests__/playerFactory.test.ts +1 -1
  76. package/appUtils/playerManager/__tests__/playerNative.test.ts +114 -0
  77. package/appUtils/playerManager/__tests__/playerNativeCommand.test.ts +62 -0
  78. package/appUtils/playerManager/__tests__/playerToolbox.test.ts +97 -0
  79. package/appUtils/playerManager/__tests__/usePlayerContent.test.tsx +64 -0
  80. package/appUtils/playerManager/__tests__/usePlayerControllerSetup.test.tsx +62 -0
  81. package/appUtils/playerManager/{conts.ts → const.ts} +33 -0
  82. package/appUtils/playerManager/index.ts +8 -1
  83. package/appUtils/playerManager/player.ts +125 -3
  84. package/appUtils/playerManager/playerFactory.ts +1 -1
  85. package/appUtils/playerManager/playerNative.ts +28 -4
  86. package/appUtils/playerManager/playerNativeCommand.ts +3 -6
  87. package/appUtils/playerManager/playerToolbox.ts +157 -0
  88. package/appUtils/playerManager/usePlayerContent.tsx +43 -0
  89. package/appUtils/playerManager/usePlayerControllerSetup.tsx +7 -1
  90. package/appUtils/playerManager/userLanguagePreference.ts +1 -1
  91. package/arrayUtils/__tests__/{anyThruthy.test.ts → anyTruthy.test.ts} +8 -0
  92. package/arrayUtils/__tests__/arrayUtils.test.ts +165 -108
  93. package/arrayUtils/__tests__/isEmptyArray.test.ts +11 -0
  94. package/arrayUtils/__tests__/isFilledArray.test.ts +12 -0
  95. package/arrayUtils/__tests__/isFirst.test.ts +17 -0
  96. package/arrayUtils/__tests__/isIndexInRange.test.ts +14 -0
  97. package/arrayUtils/__tests__/isLast.test.ts +31 -0
  98. package/arrayUtils/__tests__/makeListOf.test.ts +31 -0
  99. package/arrayUtils/__tests__/makeListOfIndexes.test.ts +20 -0
  100. package/arrayUtils/__tests__/reorderByIds.test.ts +63 -0
  101. package/arrayUtils/__tests__/sample.test.ts +61 -0
  102. package/arrayUtils/index.ts +119 -20
  103. package/bottomSheet/__tests__/scrollContext.test.tsx +35 -0
  104. package/bottomSheet/index.ts +17 -0
  105. package/cellUtils/__tests__/getButtonsEnabled.test.ts +28 -0
  106. package/cellUtils/__tests__/getCellNavigationEnabled.test.ts +36 -0
  107. package/cellUtils/__tests__/getIsCellNavigationEnabled.test.ts +35 -0
  108. package/cellUtils/__tests__/isCellNavigationEnabled.test.ts +21 -0
  109. package/cellUtils/index.ts +73 -8
  110. package/colorUtils/__tests__/isTransparentColor.test.ts +76 -0
  111. package/colorUtils/__tests__/isValidColor.test.ts +70 -0
  112. package/colorUtils/index.ts +50 -0
  113. package/configurationUtils/__tests__/manifestKeyParser.test.ts +21 -0
  114. package/configurationUtils/__tests__/modalBlocksParser.test.ts +392 -0
  115. package/configurationUtils/manifestKeyParser.ts +50 -10
  116. package/configurationUtils/modalBlocksParser.ts +252 -0
  117. package/entryActions/__tests__/buildEntryActions.test.ts +235 -0
  118. package/entryActions/aliasPresentation.ts +108 -0
  119. package/entryActions/buildEntryActions.ts +167 -0
  120. package/entryActions/index.ts +15 -0
  121. package/entryActions/types.ts +39 -0
  122. package/focusManager/FocusManager.ts +26 -0
  123. package/focusManager/__tests__/FocusManager.test.ts +9 -0
  124. package/localizationUtils/index.ts +12 -6
  125. package/logger/__tests__/xrayToolbox.test.ts +69 -0
  126. package/logger/xrayToolbox.ts +67 -0
  127. package/manifestUtils/__tests__/player.audioControls.test.js +158 -0
  128. package/manifestUtils/_internals/index.js +14 -3
  129. package/manifestUtils/defaultManifestConfigurations/generalContent.js +41 -0
  130. package/manifestUtils/defaultManifestConfigurations/player.js +357 -20
  131. package/manifestUtils/fieldUtils/__tests__/fieldUtils.test.js +84 -0
  132. package/manifestUtils/fieldUtils/index.js +125 -0
  133. package/manifestUtils/index.js +3 -0
  134. package/manifestUtils/keys.js +15 -1
  135. package/manifestUtils/mobileAction/button/index.js +16 -0
  136. package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +21 -0
  137. package/manifestUtils/mobileAction/container/index.js +3 -1
  138. package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +1 -1
  139. package/manifestUtils/mobileAction/groups/defaults.js +5 -2
  140. package/manifestUtils/modalBlocks.js +304 -0
  141. package/manifestUtils/platformIsTV.js +1 -0
  142. package/modalState/ContentViewModel.ts +113 -0
  143. package/modalState/EditableCollection.ts +17 -0
  144. package/modalState/EditableCollectionRegistry.ts +51 -0
  145. package/modalState/ModalOrchestrator.ts +212 -0
  146. package/modalState/RemoteEditableCollection.ts +235 -0
  147. package/modalState/__tests__/ContentViewModel.editable.test.ts +69 -0
  148. package/modalState/__tests__/ContentViewModel.test.ts +165 -0
  149. package/modalState/__tests__/EditableCollectionRegistry.test.ts +112 -0
  150. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +63 -0
  151. package/modalState/__tests__/RemoteEditableCollection.test.ts +415 -0
  152. package/modalState/__tests__/index.test.ts +30 -1
  153. package/modalState/__tests__/useBottomSheetContent.test.ts +349 -0
  154. package/modalState/__tests__/useModalStoreState.test.ts +142 -0
  155. package/modalState/index.ts +35 -83
  156. package/modalState/store.ts +109 -0
  157. package/modalState/types.ts +162 -0
  158. package/modalState/useBottomSheetContent.ts +109 -0
  159. package/navigationUtils/xrayNavigator.ts +12 -0
  160. package/numberUtils/__tests__/isMinusZero.test.ts +20 -0
  161. package/numberUtils/__tests__/isZero.test.ts +27 -0
  162. package/numberUtils/__tests__/requireNumber.test.ts +50 -0
  163. package/numberUtils/__tests__/toNumber.test.ts +27 -0
  164. package/numberUtils/__tests__/toNumberWithDefault.test.ts +64 -0
  165. package/numberUtils/__tests__/toNumberWithDefaultZero.test.ts +48 -0
  166. package/numberUtils/index.ts +27 -8
  167. package/package.json +2 -2
  168. package/pipesUtils/__tests__/buildUrlWithQuery.test.ts +33 -0
  169. package/pipesUtils/__tests__/withPipesEndpoint.test.tsx +56 -0
  170. package/pipesUtils/index.ts +1 -0
  171. package/pipesUtils/withPipesEndpoint.tsx +54 -0
  172. package/playerUtils/__tests__/contentDataKeys.test.ts +297 -0
  173. package/playerUtils/__tests__/resolvePlayerActions.test.ts +138 -0
  174. package/playerUtils/__tests__/resolvePlayerTitleSubtitle.test.ts +180 -0
  175. package/playerUtils/contentDataKeys.ts +134 -0
  176. package/playerUtils/index.ts +37 -16
  177. package/playerUtils/isAudioItem.ts +26 -0
  178. package/playerUtils/resolvePlayerActions.ts +71 -0
  179. package/playerUtils/resolvePlayerTitleSubtitle.ts +76 -0
  180. package/pluginUtils/__tests__/pluginUtils.test.js +32 -0
  181. package/pluginUtils/index.ts +29 -0
  182. package/reactHooks/__tests__/getPluginConfiguration.test.ts +82 -0
  183. package/reactHooks/actions/index.ts +117 -2
  184. package/reactHooks/cell-click/__tests__/index.test.js +64 -0
  185. package/reactHooks/cell-click/index.ts +39 -24
  186. package/reactHooks/connection/index.tsx +8 -5
  187. package/reactHooks/feed/__mocks__/useMarkPipesDataStale.ts +4 -0
  188. package/reactHooks/feed/__tests__/useInflatedUrl.test.tsx +25 -0
  189. package/reactHooks/feed/__tests__/useMarkPipesDataStale.test.tsx +243 -0
  190. package/reactHooks/feed/index.ts +5 -1
  191. package/reactHooks/feed/useBatchLoading.ts +9 -1
  192. package/reactHooks/feed/useMarkPipesDataStale.ts +97 -0
  193. package/reactHooks/index.ts +2 -0
  194. package/reactHooks/layout/index.ts +1 -1
  195. package/reactHooks/navigation/__mocks__/index.ts +4 -0
  196. package/reactHooks/navigation/__tests__/index.test.tsx +176 -1
  197. package/reactHooks/navigation/__tests__/useIsScreenActive.test.ts +42 -0
  198. package/reactHooks/navigation/__tests__/usePresentScreen.test.ts +154 -0
  199. package/reactHooks/navigation/index.ts +3 -1
  200. package/reactHooks/navigation/useIsScreenActive.ts +29 -8
  201. package/reactHooks/navigation/usePresentScreen.ts +157 -0
  202. package/reactHooks/navigation/useRoute.ts +22 -5
  203. package/reactHooks/state/useComponentScreenState.ts +1 -1
  204. package/reactHooks/usePluginConfiguration.ts +88 -47
  205. package/reactHooks/utils/__tests__/index.test.js +22 -2
  206. package/reactHooks/utils/index.ts +13 -2
  207. package/riverComponetsMeasurementProvider/index.tsx +15 -9
  208. package/stringUtils/__tests__/stringUtils.test.js +42 -0
  209. package/stringUtils/index.ts +2 -2
  210. package/uiActionsRegistrator/__tests__/resolveActionIdentifiers.test.ts +93 -0
  211. package/uiActionsRegistrator/__tests__/subscribe.test.ts +133 -0
  212. package/uiActionsRegistrator/__tests__/usableActionItems.test.ts +52 -0
  213. package/uiActionsRegistrator/index.ts +25 -0
  214. package/uiActionsRegistrator/registry.ts +351 -0
  215. package/uiActionsRegistrator/resolveActionIdentifiers.ts +100 -0
  216. package/uiActionsRegistrator/usableActionItems.ts +66 -0
  217. package/zappFrameworkUtils/__tests__/localStorageHelper.test.ts +146 -0
  218. package/zappFrameworkUtils/localStorageHelper.ts +19 -15
  219. package/appUtils/playerManager/usePlayerTitleSummaryOverlay.tsx +0 -56
  220. package/playerUtils/__tests__/getPlayerActionButtons.test.ts +0 -54
  221. package/playerUtils/getPlayerActionButtons.ts +0 -17
  222. package/reactHooks/feed/__mocks__/usePipesCacheReset.ts +0 -1
  223. package/reactHooks/feed/usePipesCacheReset.ts +0 -44
  224. /package/reactHooks/actions/__tests__/{index.test.js → index.test.tsx} +0 -0
@@ -1,10 +1,20 @@
1
1
  import * as React from "react";
2
- import { NativeModules, StyleSheet, View } from "react-native";
2
+ import { NativeModules, View, StyleSheet } from "react-native";
3
3
  import { getXray } from "@applicaster/zapp-react-native-utils/logger";
4
4
 
5
- import { isApplePlatform, isWeb } from "../reactUtils";
5
+ import { isApplePlatform, isWeb, isTV } from "../reactUtils";
6
6
  import { useRivers } from "../reactHooks";
7
7
 
8
+ const isTVPlatform = isTV();
9
+
10
+ const styles = StyleSheet.create({
11
+ container: isTVPlatform
12
+ ? {
13
+ flex: 1,
14
+ }
15
+ : {},
16
+ });
17
+
8
18
  const layoutReducer = (state, { payload }) => {
9
19
  return state.map((item, index, _state) => ({
10
20
  height: index === payload.index ? payload.height : item.height,
@@ -24,18 +34,13 @@ type Props = {
24
34
  type ItemProps = {
25
35
  children: React.ReactNode;
26
36
  index: number;
37
+ testID?: string;
27
38
  };
28
39
 
29
40
  type MeasurementContext = {
30
41
  onLayout: (index: number) => (event) => void;
31
42
  };
32
43
 
33
- const styles = StyleSheet.create({
34
- container: {
35
- flex: 1,
36
- },
37
- });
38
-
39
44
  const { Logger } = getXray();
40
45
 
41
46
  const logger = new Logger("general", "ui");
@@ -149,11 +154,12 @@ export const ScreenLoadingMeasurements = ({
149
154
  export const ScreenLoadingMeasurementsListItemWrapper = ({
150
155
  children,
151
156
  index,
157
+ testID,
152
158
  }: ItemProps) => {
153
159
  const { onLayout } = React.useContext(MeasurementsSettersContext);
154
160
 
155
161
  return (
156
- <View style={styles.container} onLayout={onLayout?.(index)}>
162
+ <View style={styles.container} onLayout={onLayout?.(index)} testID={testID}>
157
163
  {children}
158
164
  </View>
159
165
  );
@@ -122,6 +122,48 @@ describe("inflate string", () => {
122
122
  "this is a string with custom separators"
123
123
  );
124
124
  });
125
+
126
+ it("does not HTML-escape search variables containing single quotes or apostrophes", () => {
127
+ const string = "https://api.example.com/search?q={{search.query}}";
128
+ const data = { search: { query: "L'aventure" } };
129
+
130
+ expect(inflateString({ string, data })).toBe(
131
+ "https://api.example.com/search?q=L'aventure"
132
+ );
133
+ });
134
+
135
+ it("search context with straight apostrophe", () => {
136
+ const string =
137
+ "https://api.pc-cms.tele.quebec/api/applicaster/v1/tv/search?q={{search.q}}";
138
+
139
+ const data = { search: { q: "l'a" } };
140
+
141
+ expect(inflateString({ string, data })).toBe(
142
+ "https://api.pc-cms.tele.quebec/api/applicaster/v1/tv/search?q=l'a"
143
+ );
144
+ });
145
+
146
+ it("search context with backtick apostrophe", () => {
147
+ const string =
148
+ "https://api.pc-cms.tele.quebec/api/applicaster/v1/tv/search?q={{search.q}}";
149
+
150
+ const data = { search: { q: "l`a" } };
151
+
152
+ expect(inflateString({ string, data })).toBe(
153
+ "https://api.pc-cms.tele.quebec/api/applicaster/v1/tv/search?q=l`a"
154
+ );
155
+ });
156
+
157
+ it("search context with right single quotation mark", () => {
158
+ const string =
159
+ "https://api.pc-cms.tele.quebec/api/applicaster/v1/tv/search?q={{search.q}}";
160
+
161
+ const data = { search: { q: "l’a" } };
162
+
163
+ expect(inflateString({ string, data })).toBe(
164
+ "https://api.pc-cms.tele.quebec/api/applicaster/v1/tv/search?q=l’a"
165
+ );
166
+ });
125
167
  });
126
168
 
127
169
  describe("objectToReadableString", () => {
@@ -72,8 +72,8 @@ const valueIsNull = (value) => () => value === null;
72
72
  const encodeHelper = "encodeURL";
73
73
 
74
74
  const encodeHandlebarsExpressions = R.compose(
75
- R.replace(/{{([^}]+)}}/g, (match, p1) =>
76
- p1.includes("search") ? match : `{{{${encodeHelper} ${p1}}}}`
75
+ R.replace(/{{([^}]+)}}/g, (_, p1) =>
76
+ p1.includes("search") ? `{{{${p1}}}}` : `{{{${encodeHelper} ${p1}}}}`
77
77
  )
78
78
  );
79
79
 
@@ -0,0 +1,93 @@
1
+ import {
2
+ parseActionIdentifiers,
3
+ resolveActionIdentifiers,
4
+ } from "../resolveActionIdentifiers";
5
+ import { uiActionsRegistry } from "../registry";
6
+
7
+ const action = (identifier: string) =>
8
+ ({ identifier, action: { invokeAction: jest.fn() } }) as any;
9
+
10
+ describe("parseActionIdentifiers", () => {
11
+ it("parses comma-separated identifiers", () => {
12
+ expect(parseActionIdentifiers(" a, b , ,c ")).toEqual(["a", "b", "c"]);
13
+ });
14
+
15
+ it("parses identifier arrays", () => {
16
+ expect(parseActionIdentifiers([" a ", "", "b", " ", "c"])).toEqual([
17
+ "a",
18
+ "b",
19
+ "c",
20
+ ]);
21
+ });
22
+
23
+ it("returns empty array for nil config", () => {
24
+ expect(parseActionIdentifiers(null)).toEqual([]);
25
+ expect(parseActionIdentifiers(undefined)).toEqual([]);
26
+ });
27
+ });
28
+
29
+ describe("resolveActionIdentifiers", () => {
30
+ const entry = { id: "entry-1" } as any;
31
+
32
+ beforeEach(() => {
33
+ jest
34
+ .spyOn(uiActionsRegistry, "getActions")
35
+ .mockImplementation((identifier) => [action(identifier)] as any);
36
+ });
37
+
38
+ afterEach(() => {
39
+ jest.restoreAllMocks();
40
+ });
41
+
42
+ it("resolves through registry by default", () => {
43
+ const result = resolveActionIdentifiers({
44
+ identifiers: "favorites,share",
45
+ entry,
46
+ });
47
+
48
+ expect(result.map((item) => item.identifier)).toEqual([
49
+ "favorites",
50
+ "share",
51
+ ]);
52
+
53
+ expect(uiActionsRegistry.getActions).toHaveBeenCalledWith(
54
+ "favorites",
55
+ entry
56
+ );
57
+
58
+ expect(uiActionsRegistry.getActions).toHaveBeenCalledWith("share", entry);
59
+ });
60
+
61
+ it("expands entry_actions through the caller, not the registry", () => {
62
+ const expanded = [action("entry_action_0"), action("entry_action_1")];
63
+ const expandEntryActions = jest.fn(() => expanded);
64
+
65
+ const unregister = uiActionsRegistry.registerAction("share", {
66
+ invokeAction: jest.fn(),
67
+ });
68
+
69
+ try {
70
+ const result = resolveActionIdentifiers({
71
+ identifiers: "entry_actions,share",
72
+ entry,
73
+ expandEntryActions,
74
+ });
75
+
76
+ expect(expandEntryActions).toHaveBeenCalledWith(entry);
77
+
78
+ expect(result.map((item) => item.identifier)).toEqual([
79
+ "entry_action_0",
80
+ "entry_action_1",
81
+ "share",
82
+ ]);
83
+ } finally {
84
+ unregister();
85
+ }
86
+ });
87
+
88
+ it("resolves nothing for entry_actions when the caller cannot expand it", () => {
89
+ expect(
90
+ resolveActionIdentifiers({ identifiers: "entry_actions", entry })
91
+ ).toEqual([]);
92
+ });
93
+ });
@@ -0,0 +1,133 @@
1
+ import { isActionAvailableFor, uiActionsRegistry } from "../registry";
2
+
3
+ const value = { invokeAction: jest.fn() };
4
+
5
+ describe("uiActionsRegistry.subscribe", () => {
6
+ it("fires on registration and on unregistration", () => {
7
+ const listener = jest.fn();
8
+ const unsubscribe = uiActionsRegistry.subscribe(listener);
9
+
10
+ const unregister = uiActionsRegistry.registerAction("late-action", value);
11
+
12
+ expect(listener).toHaveBeenCalledTimes(1);
13
+
14
+ unregister();
15
+
16
+ expect(listener).toHaveBeenCalledTimes(2);
17
+
18
+ unsubscribe();
19
+ });
20
+
21
+ it("lets a listener see the action it was told about", () => {
22
+ let seen: string[] = [];
23
+
24
+ const unsubscribe = uiActionsRegistry.subscribe(() => {
25
+ seen = uiActionsRegistry
26
+ .getActions("late-action", { id: "e1" } as any)
27
+ .map((item) => item.identifier);
28
+ });
29
+
30
+ const unregister = uiActionsRegistry.registerAction("late-action", value);
31
+
32
+ expect(seen).toEqual(["late-action"]);
33
+
34
+ unregister();
35
+ unsubscribe();
36
+ });
37
+
38
+ it("stops calling a listener once unsubscribed", () => {
39
+ const listener = jest.fn();
40
+
41
+ uiActionsRegistry.subscribe(listener)();
42
+
43
+ uiActionsRegistry.registerAction("another-action", value)();
44
+
45
+ expect(listener).not.toHaveBeenCalled();
46
+ });
47
+
48
+ it("survives a listener that unsubscribes itself while being notified", () => {
49
+ const other = jest.fn();
50
+
51
+ const unsubscribeOther = uiActionsRegistry.subscribe(other);
52
+
53
+ const unsubscribeSelf = uiActionsRegistry.subscribe(() => {
54
+ unsubscribeSelf();
55
+ });
56
+
57
+ expect(() =>
58
+ uiActionsRegistry.registerAction("self-removing", value)()
59
+ ).not.toThrow();
60
+
61
+ expect(other).toHaveBeenCalled();
62
+
63
+ unsubscribeOther();
64
+ });
65
+ });
66
+
67
+ describe("isActionAvailableFor", () => {
68
+ const entry = { id: "e1" } as any;
69
+
70
+ it("shows an action that states no opinion", () => {
71
+ expect(isActionAvailableFor({ invokeAction: jest.fn() }, entry)).toBe(true);
72
+ });
73
+
74
+ it.each([
75
+ ["isSupportDownloads"],
76
+ ["isActionSupported"],
77
+ ["isActionAvailable"],
78
+ ])("honours %s", (predicate) => {
79
+ expect(
80
+ isActionAvailableFor(
81
+ { invokeAction: jest.fn(), [predicate]: () => false } as any,
82
+ entry
83
+ )
84
+ ).toBe(false);
85
+ });
86
+
87
+ it("asks only the first predicate the action defines", () => {
88
+ const isActionAvailable = jest.fn(() => true);
89
+
90
+ expect(
91
+ isActionAvailableFor(
92
+ {
93
+ invokeAction: jest.fn(),
94
+ isSupportDownloads: () => false,
95
+ isActionAvailable,
96
+ } as any,
97
+ entry
98
+ )
99
+ ).toBe(false);
100
+
101
+ expect(isActionAvailable).not.toHaveBeenCalled();
102
+ });
103
+
104
+ it("treats a missing action as unavailable-free", () => {
105
+ expect(isActionAvailableFor(undefined, entry)).toBe(true);
106
+ });
107
+ });
108
+
109
+ describe("uiActionsRegistry.notifyProviderChanged", () => {
110
+ it("notifies subscribers for a registered provider", () => {
111
+ const unregister = uiActionsRegistry.registerAction("live-action", value);
112
+ const listener = jest.fn();
113
+ const unsubscribe = uiActionsRegistry.subscribe(listener);
114
+
115
+ uiActionsRegistry.notifyProviderChanged("live-action");
116
+
117
+ expect(listener).toHaveBeenCalledTimes(1);
118
+
119
+ unsubscribe();
120
+ unregister();
121
+ });
122
+
123
+ it("ignores names that are not registered", () => {
124
+ const listener = jest.fn();
125
+ const unsubscribe = uiActionsRegistry.subscribe(listener);
126
+
127
+ uiActionsRegistry.notifyProviderChanged("unknown-action");
128
+
129
+ expect(listener).not.toHaveBeenCalled();
130
+
131
+ unsubscribe();
132
+ });
133
+ });
@@ -0,0 +1,52 @@
1
+ import { usableActionItems } from "../usableActionItems";
2
+
3
+ const action = (identifier: string) =>
4
+ ({ identifier, action: { invokeAction: jest.fn() } }) as any;
5
+
6
+ describe("usableActionItems", () => {
7
+ it("keeps a usable list untouched, in order", () => {
8
+ const base = [action("share"), action("favorites")];
9
+
10
+ expect(usableActionItems(base).map((item) => item.identifier)).toEqual([
11
+ "share",
12
+ "favorites",
13
+ ]);
14
+ });
15
+
16
+ it("keeps the first of a repeated identifier", () => {
17
+ const firstShare = action("share");
18
+ const secondShare = action("share");
19
+
20
+ const result = usableActionItems([
21
+ firstShare,
22
+ action("favorites"),
23
+ secondShare,
24
+ ]);
25
+
26
+ expect(result.map((item) => item.identifier)).toEqual([
27
+ "share",
28
+ "favorites",
29
+ ]);
30
+
31
+ expect(result[0]).toBe(firstShare);
32
+ });
33
+
34
+ it("drops items that cannot be invoked or have no identifier", () => {
35
+ const result = usableActionItems([
36
+ action("share"),
37
+ { identifier: "blank" } as any,
38
+ { identifier: "no-invoke", action: {} } as any,
39
+ { action: { invokeAction: jest.fn() } } as any,
40
+ action("entry_action_0"),
41
+ ]);
42
+
43
+ expect(result.map((item) => item.identifier)).toEqual([
44
+ "share",
45
+ "entry_action_0",
46
+ ]);
47
+ });
48
+
49
+ it("tolerates a missing list", () => {
50
+ expect(usableActionItems(undefined as any)).toEqual([]);
51
+ });
52
+ });
@@ -0,0 +1,25 @@
1
+ export {
2
+ canRenderActionIcon,
3
+ resolveActionAsset,
4
+ isActionAvailableFor,
5
+ observeEntryState,
6
+ uiActionsRegistry,
7
+ } from "./registry";
8
+
9
+ export type {
10
+ RegisteredAction,
11
+ RegisteredActionSource,
12
+ RegisteredActionValue,
13
+ RegistryChangeListener,
14
+ UIActionProvider,
15
+ } from "./registry";
16
+
17
+ export { usableActionItems } from "./usableActionItems";
18
+
19
+ export {
20
+ ENTRY_ACTIONS_IDENTIFIER,
21
+ parseActionIdentifiers,
22
+ resolveActionIdentifiers,
23
+ } from "./resolveActionIdentifiers";
24
+
25
+ export type { ResolveActionIdentifiersParams } from "./resolveActionIdentifiers";