@applicaster/zapp-react-native-utils 16.0.0-alpha.5439705823 → 16.0.0-alpha.5548956531

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 (95) hide show
  1. package/actionsExecutor/ActionExecutor.ts +8 -10
  2. package/actionsExecutor/ActionExecutorContext.tsx +44 -345
  3. package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +22 -0
  4. package/actionsExecutor/actions/__tests__/loadItemsFromSource.test.ts +88 -0
  5. package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +76 -0
  6. package/actionsExecutor/actions/__tests__/showToast.test.ts +63 -0
  7. package/actionsExecutor/actions/appRestart.ts +24 -0
  8. package/actionsExecutor/actions/confirmDialog.ts +53 -0
  9. package/actionsExecutor/actions/dismissBottomSheet.ts +22 -0
  10. package/actionsExecutor/actions/index.ts +34 -0
  11. package/actionsExecutor/actions/localStorageRemove.ts +27 -0
  12. package/actionsExecutor/actions/localStorageSet.ts +28 -0
  13. package/actionsExecutor/actions/localStorageToggleFlag.ts +28 -0
  14. package/actionsExecutor/actions/navigateToScreen.ts +123 -0
  15. package/actionsExecutor/actions/openBottomSheet.ts +318 -0
  16. package/actionsExecutor/actions/refreshComponent.ts +85 -0
  17. package/actionsExecutor/actions/screenSetVariable.ts +35 -0
  18. package/actionsExecutor/actions/screenToggleFlag.ts +38 -0
  19. package/actionsExecutor/actions/sendCloudEvent.ts +96 -0
  20. package/actionsExecutor/actions/sessionStorageRemove.ts +19 -0
  21. package/actionsExecutor/actions/sessionStorageSet.ts +20 -0
  22. package/actionsExecutor/actions/sessionStorageToggleFlag.ts +15 -0
  23. package/actionsExecutor/actions/showToast.ts +31 -0
  24. package/actionsExecutor/actions/switchLayout.ts +40 -0
  25. package/actionsExecutor/types.ts +59 -0
  26. package/analyticsUtils/PlayerAnalyticsManager.ts +1 -1
  27. package/appUtils/contextKeysManager/__tests__/getKey/failure.test.ts +1 -1
  28. package/appUtils/contextKeysManager/__tests__/removeKey/failure.test.ts +1 -1
  29. package/appUtils/contextKeysManager/__tests__/setKey/failure/invalidKey.test.ts +4 -4
  30. package/appUtils/contextKeysManager/index.ts +1 -1
  31. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +12 -6
  32. package/appUtils/playerManager/__tests__/playerFactory.test.ts +1 -1
  33. package/appUtils/playerManager/{conts.ts → const.ts} +20 -0
  34. package/appUtils/playerManager/index.ts +1 -1
  35. package/appUtils/playerManager/player.ts +1 -1
  36. package/appUtils/playerManager/playerFactory.ts +1 -1
  37. package/appUtils/playerManager/playerNative.ts +1 -1
  38. package/appUtils/playerManager/usePlayerControllerSetup.tsx +1 -1
  39. package/appUtils/playerManager/userLanguagePreference.ts +1 -1
  40. package/arrayUtils/__tests__/{anyThruthy.test.ts → anyTruthy.test.ts} +8 -0
  41. package/arrayUtils/__tests__/arrayUtils.test.ts +165 -108
  42. package/arrayUtils/__tests__/isEmptyArray.test.ts +11 -0
  43. package/arrayUtils/__tests__/isFilledArray.test.ts +12 -0
  44. package/arrayUtils/__tests__/isFirst.test.ts +17 -0
  45. package/arrayUtils/__tests__/isIndexInRange.test.ts +14 -0
  46. package/arrayUtils/__tests__/isLast.test.ts +31 -0
  47. package/arrayUtils/__tests__/makeListOf.test.ts +31 -0
  48. package/arrayUtils/__tests__/makeListOfIndexes.test.ts +20 -0
  49. package/arrayUtils/__tests__/sample.test.ts +61 -0
  50. package/arrayUtils/index.ts +57 -20
  51. package/bottomSheet/__tests__/scrollContext.test.tsx +35 -0
  52. package/bottomSheet/index.ts +17 -0
  53. package/manifestUtils/defaultManifestConfigurations/player.js +19 -19
  54. package/manifestUtils/keys.js +8 -0
  55. package/manifestUtils/mobileAction/button/index.js +16 -0
  56. package/manifestUtils/mobileAction/container/index.js +3 -1
  57. package/manifestUtils/mobileAction/groups/defaults.js +3 -1
  58. package/modalState/ContentViewModel.ts +111 -0
  59. package/modalState/EditableCollection.ts +17 -0
  60. package/modalState/EditableCollectionRegistry.ts +51 -0
  61. package/modalState/ModalOrchestrator.ts +198 -0
  62. package/modalState/RemoteEditableCollection.ts +189 -0
  63. package/modalState/__tests__/ContentViewModel.editable.test.ts +69 -0
  64. package/modalState/__tests__/ContentViewModel.test.ts +165 -0
  65. package/modalState/__tests__/EditableCollectionRegistry.test.ts +112 -0
  66. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +28 -0
  67. package/modalState/__tests__/RemoteEditableCollection.test.ts +259 -0
  68. package/modalState/__tests__/useBottomSheetContent.test.ts +372 -0
  69. package/modalState/index.ts +30 -83
  70. package/modalState/store.ts +102 -0
  71. package/modalState/types.ts +110 -0
  72. package/modalState/useBottomSheetContent.ts +98 -0
  73. package/numberUtils/__tests__/isMinusZero.test.ts +20 -0
  74. package/numberUtils/__tests__/isZero.test.ts +27 -0
  75. package/numberUtils/__tests__/requireNumber.test.ts +50 -0
  76. package/numberUtils/__tests__/toNumber.test.ts +27 -0
  77. package/numberUtils/__tests__/toNumberWithDefault.test.ts +64 -0
  78. package/numberUtils/__tests__/toNumberWithDefaultZero.test.ts +48 -0
  79. package/numberUtils/index.ts +27 -8
  80. package/package.json +2 -2
  81. package/reactHooks/actions/index.ts +51 -1
  82. package/reactHooks/cell-click/index.ts +3 -3
  83. package/reactHooks/feed/__tests__/useInflatedUrl.test.tsx +25 -0
  84. package/reactHooks/layout/index.ts +1 -1
  85. package/reactHooks/navigation/__mocks__/index.ts +4 -0
  86. package/reactHooks/navigation/__tests__/useIsScreenActive.test.ts +42 -0
  87. package/reactHooks/navigation/index.ts +1 -1
  88. package/reactHooks/navigation/useIsScreenActive.ts +29 -8
  89. package/reactHooks/state/useComponentScreenState.ts +1 -1
  90. package/stringUtils/__tests__/stringUtils.test.js +42 -0
  91. package/stringUtils/index.ts +2 -2
  92. package/uiActionsRegistrator/index.ts +203 -0
  93. package/zappFrameworkUtils/__tests__/localStorageHelper.test.ts +146 -0
  94. package/zappFrameworkUtils/localStorageHelper.ts +9 -13
  95. /package/reactHooks/actions/__tests__/{index.test.js → index.test.tsx} +0 -0
@@ -0,0 +1,112 @@
1
+ import {
2
+ EditableCollectionRegistry,
3
+ resolveEditableCollection,
4
+ } from "../EditableCollectionRegistry";
5
+ import { RemoteEditableCollection } from "../RemoteEditableCollection";
6
+ import { EditableCollection } from "../EditableCollection";
7
+ import { BehaviorSubject } from "rxjs";
8
+ import type { Content, MenuItem } from "../types";
9
+
10
+ function fakeCollection(): EditableCollection {
11
+ return {
12
+ items$: new BehaviorSubject<MenuItem[]>([]).asObservable(),
13
+ operations: [],
14
+ };
15
+ }
16
+
17
+ const refetch = jest.fn().mockResolvedValue(undefined);
18
+
19
+ describe("EditableCollectionRegistry", () => {
20
+ beforeEach(() => {
21
+ // reset the static registry between tests
22
+ (EditableCollectionRegistry as any).providers = {};
23
+ jest.clearAllMocks();
24
+ });
25
+
26
+ it("registers and resolves a provider factory (new instance per call)", () => {
27
+ const instance = fakeCollection();
28
+ const factory = jest.fn(() => instance);
29
+ EditableCollectionRegistry.register("@{provider/x}", factory);
30
+
31
+ const resolved = EditableCollectionRegistry.getProvider("@{provider/x}");
32
+
33
+ expect(resolved).toBe(instance);
34
+ expect(factory).toHaveBeenCalledTimes(1);
35
+ });
36
+
37
+ it("returns null for an unknown provider token", () => {
38
+ expect(EditableCollectionRegistry.getProvider("@{provider/missing}")).toBe(
39
+ null
40
+ );
41
+ });
42
+ });
43
+
44
+ describe("resolveEditableCollection", () => {
45
+ beforeEach(() => {
46
+ (EditableCollectionRegistry as any).providers = {};
47
+ jest.clearAllMocks();
48
+ });
49
+
50
+ it("returns null when there are no dynamicCollection options", () => {
51
+ const content = { items: [] } as unknown as Content;
52
+
53
+ expect(resolveEditableCollection(content, refetch)).toBe(null);
54
+ });
55
+
56
+ it("resolves a local provider by token", () => {
57
+ const instance = fakeCollection();
58
+ EditableCollectionRegistry.register("@{provider/queue}", () => instance);
59
+
60
+ const content = {
61
+ items: [],
62
+ dynamicCollection: {
63
+ operations: ["remove"],
64
+ provider: "@{provider/queue}",
65
+ },
66
+ } as unknown as Content;
67
+
68
+ expect(resolveEditableCollection(content, refetch)).toBe(instance);
69
+ });
70
+
71
+ it("returns null (and does not throw) for an unregistered provider token", () => {
72
+ const content = {
73
+ items: [],
74
+ dynamicCollection: {
75
+ operations: ["remove"],
76
+ provider: "@{provider/unregistered}",
77
+ },
78
+ } as unknown as Content;
79
+
80
+ expect(resolveEditableCollection(content, refetch)).toBe(null);
81
+ });
82
+
83
+ it("builds a RemoteEditableCollection when a postUrl is present", () => {
84
+ const content = {
85
+ items: [],
86
+ dynamicCollection: {
87
+ operations: ["remove"],
88
+ postUrl: "https://x/cloud-events",
89
+ },
90
+ } as unknown as Content;
91
+
92
+ expect(resolveEditableCollection(content, refetch)).toBeInstanceOf(
93
+ RemoteEditableCollection
94
+ );
95
+ });
96
+
97
+ it("prefers a provider token over postUrl", () => {
98
+ const instance = fakeCollection();
99
+ EditableCollectionRegistry.register("@{provider/queue}", () => instance);
100
+
101
+ const content = {
102
+ items: [],
103
+ dynamicCollection: {
104
+ operations: ["remove"],
105
+ provider: "@{provider/queue}",
106
+ postUrl: "https://x/cloud-events",
107
+ },
108
+ } as unknown as Content;
109
+
110
+ expect(resolveEditableCollection(content, refetch)).toBe(instance);
111
+ });
112
+ });
@@ -0,0 +1,28 @@
1
+ import { modalOrchestrator } from "../ModalOrchestrator";
2
+ import { modalStore } from "../store";
3
+
4
+ describe("ModalOrchestrator phase 3", () => {
5
+ beforeEach(() => {
6
+ modalOrchestrator.close();
7
+ modalStore.getState().dismissModal();
8
+ });
9
+
10
+ it("does not forward styleVariant", () => {
11
+ modalOrchestrator.open({
12
+ content: {
13
+ title: "Playlists",
14
+ items: [{ title: "Favorites" }],
15
+ role: "collection_selector",
16
+ behavior: { selectMode: "multi", currentSelection: [] },
17
+ },
18
+ });
19
+
20
+ const props = (modalStore.getState().modalState.screen as any)
21
+ ?.modalBottomSheetContentProps;
22
+
23
+ expect(props.styleVariant).toBeUndefined();
24
+ expect(props.viewModel).toBeDefined();
25
+ expect(props.viewModel.getState().role).toBe("collection_selector");
26
+ expect(props.title).toBe("Playlists");
27
+ });
28
+ });
@@ -0,0 +1,259 @@
1
+ import { RemoteEditableCollection } from "../RemoteEditableCollection";
2
+ import { actionExecutor } from "../../actionsExecutor/ActionExecutor";
3
+ import type { Content, MenuItem } from "../types";
4
+
5
+ jest.mock("../../actionsExecutor/ActionExecutor", () => ({
6
+ actionExecutor: { handleAction: jest.fn() },
7
+ ActionResult: { Success: "success", Error: "error" },
8
+ }));
9
+
10
+ const handleAction = actionExecutor.handleAction as jest.Mock;
11
+
12
+ /**
13
+ * BehaviorSubject emits its current value synchronously on subscribe, so this
14
+ * returns the latest items snapshot.
15
+ */
16
+ function currentItems(col: RemoteEditableCollection): MenuItem[] {
17
+ let items: MenuItem[] = [];
18
+ const sub = col.items$.subscribe((i) => (items = i));
19
+ sub.unsubscribe();
20
+
21
+ return items;
22
+ }
23
+
24
+ const removeAction = { type: "sendCloudEvent", options: { foo: "bar" } };
25
+
26
+ const itemWithRemove: MenuItem = {
27
+ id: "1",
28
+ title: "A",
29
+ entryActions: [{ button: { alias: "remove_item" }, actions: [removeAction] }],
30
+ };
31
+
32
+ const itemPlain: MenuItem = { id: "2", title: "B" };
33
+
34
+ function makeCollection(
35
+ items: MenuItem[],
36
+ refetch = jest.fn().mockResolvedValue(undefined),
37
+ operations = ["remove", "reorder"]
38
+ ) {
39
+ const content = {
40
+ items,
41
+ dynamicCollection: { operations, postUrl: "https://x/cloud-events" },
42
+ } as unknown as Content;
43
+
44
+ return { col: new RemoteEditableCollection(content, refetch), refetch };
45
+ }
46
+
47
+ describe("RemoteEditableCollection", () => {
48
+ beforeEach(() => jest.clearAllMocks());
49
+
50
+ it("exposes the declared operations and initial items", () => {
51
+ const { col } = makeCollection([itemWithRemove, itemPlain]);
52
+
53
+ expect(col.operations).toEqual(["remove", "reorder"]);
54
+ expect(currentItems(col)).toEqual([itemWithRemove, itemPlain]);
55
+ });
56
+
57
+ describe("remove", () => {
58
+ it("optimistically removes, dispatches the aliased action, then refetches", async () => {
59
+ handleAction.mockResolvedValue("success");
60
+ const { col, refetch } = makeCollection([itemWithRemove, itemPlain]);
61
+
62
+ await col.remove(0);
63
+
64
+ expect(handleAction).toHaveBeenCalledWith(removeAction, {
65
+ entry: itemWithRemove,
66
+ });
67
+
68
+ expect(refetch).toHaveBeenCalledTimes(1);
69
+ expect(currentItems(col)).toEqual([itemPlain]);
70
+ });
71
+
72
+ it("rolls back to the snapshot when the action dispatch fails", async () => {
73
+ handleAction.mockRejectedValue(new Error("network"));
74
+ const { col, refetch } = makeCollection([itemWithRemove, itemPlain]);
75
+
76
+ await col.remove(0);
77
+
78
+ // optimistic removal reverted
79
+ expect(currentItems(col)).toEqual([itemWithRemove, itemPlain]);
80
+ // refetch never reached on failure
81
+ expect(refetch).not.toHaveBeenCalled();
82
+ });
83
+
84
+ it("lets the refetched server result replace the optimistic list", async () => {
85
+ handleAction.mockResolvedValue("success");
86
+ const serverItems: MenuItem[] = [{ id: "99", title: "server truth" }];
87
+
88
+ const refetch = jest.fn();
89
+ const { col } = makeCollection([itemWithRemove, itemPlain], refetch);
90
+
91
+ // Emulate ContentViewModel.refetch pushing authoritative items via sync()
92
+ refetch.mockImplementation(async () => {
93
+ col.sync(serverItems);
94
+ });
95
+
96
+ await col.remove(0);
97
+
98
+ expect(currentItems(col)).toEqual(serverItems);
99
+ });
100
+
101
+ it("is a no-op for an out-of-range index", async () => {
102
+ const { col, refetch } = makeCollection([itemWithRemove]);
103
+
104
+ await col.remove(5);
105
+
106
+ expect(handleAction).not.toHaveBeenCalled();
107
+ expect(refetch).not.toHaveBeenCalled();
108
+ expect(currentItems(col)).toEqual([itemWithRemove]);
109
+ });
110
+
111
+ it("skips dispatch (no throw) when no matching alias exists", async () => {
112
+ const { col, refetch } = makeCollection([itemPlain, itemWithRemove]);
113
+
114
+ await col.remove(0); // itemPlain has no entryActions
115
+
116
+ expect(handleAction).not.toHaveBeenCalled();
117
+ // still optimistic-removes and refetches (server is source of truth)
118
+ expect(refetch).toHaveBeenCalledTimes(1);
119
+ expect(currentItems(col)).toEqual([itemWithRemove]);
120
+ });
121
+ });
122
+
123
+ describe("move", () => {
124
+ it("optimistically reorders and dispatches reorder_item with itemIds", async () => {
125
+ handleAction.mockResolvedValue("success");
126
+
127
+ const itemA: MenuItem = {
128
+ id: "2",
129
+ title: "B",
130
+ entryActions: [
131
+ { button: { alias: "reorder_item" }, actions: [removeAction] },
132
+ ],
133
+ };
134
+
135
+ const moved: MenuItem = {
136
+ id: "3",
137
+ title: "C",
138
+ entryActions: [
139
+ { button: { alias: "reorder_item" }, actions: [removeAction] },
140
+ ],
141
+ };
142
+
143
+ const { col, refetch } = makeCollection([moved, itemA]);
144
+
145
+ await col.move(["2", "3"]);
146
+
147
+ expect(currentItems(col)).toEqual([itemA, moved]);
148
+
149
+ expect(handleAction).toHaveBeenCalledWith(
150
+ {
151
+ ...removeAction,
152
+ options: {
153
+ ...removeAction.options,
154
+ itemIds: ["2", "3"],
155
+ data: {
156
+ ...removeAction.options?.data,
157
+ itemIds: ["2", "3"],
158
+ },
159
+ },
160
+ },
161
+ { entry: itemA }
162
+ );
163
+
164
+ expect(refetch).toHaveBeenCalledTimes(1);
165
+ });
166
+
167
+ it("rolls back on failure", async () => {
168
+ handleAction.mockRejectedValue(new Error("nope"));
169
+
170
+ const itemA: MenuItem = {
171
+ id: "2",
172
+ title: "B",
173
+ entryActions: [
174
+ { button: { alias: "reorder_item" }, actions: [removeAction] },
175
+ ],
176
+ };
177
+
178
+ const moved: MenuItem = {
179
+ id: "3",
180
+ title: "C",
181
+ entryActions: [
182
+ { button: { alias: "reorder_item" }, actions: [removeAction] },
183
+ ],
184
+ };
185
+
186
+ const { col } = makeCollection([moved, itemA]);
187
+
188
+ await col.move(["2", "3"]);
189
+
190
+ expect(currentItems(col)).toEqual([moved, itemA]);
191
+ });
192
+
193
+ it("is a no-op for empty or non-existent IDs", async () => {
194
+ const { col, refetch } = makeCollection([itemWithRemove, itemPlain]);
195
+
196
+ await col.move([]);
197
+
198
+ expect(refetch).not.toHaveBeenCalled();
199
+ expect(currentItems(col)).toEqual([itemWithRemove, itemPlain]);
200
+ });
201
+ });
202
+
203
+ describe("sync", () => {
204
+ it("replaces the current items with the provided list", () => {
205
+ const { col } = makeCollection([itemWithRemove]);
206
+ const next: MenuItem[] = [{ id: "7", title: "next" }];
207
+
208
+ col.sync(next);
209
+
210
+ expect(currentItems(col)).toEqual(next);
211
+ });
212
+ });
213
+
214
+ describe("deleteCollection", () => {
215
+ it("optimistically clears items and calls refetch", async () => {
216
+ handleAction.mockResolvedValue("success");
217
+ const { col, refetch } = makeCollection([itemWithRemove, itemPlain]);
218
+
219
+ await col.deleteCollection();
220
+
221
+ expect(currentItems(col)).toEqual([]);
222
+ expect(refetch).toHaveBeenCalledTimes(1);
223
+ });
224
+ });
225
+
226
+ describe("add", () => {
227
+ it("dispatches actions from dynamicCollection.events.add and refetches", async () => {
228
+ handleAction.mockResolvedValue("success");
229
+
230
+ const addAction = {
231
+ type: "sendCloudEvent",
232
+ options: {
233
+ url: "https://x/cloud-events",
234
+ type: "com.applicaster.collection.create.v1",
235
+ subject: "create_collection",
236
+ },
237
+ };
238
+
239
+ const content = {
240
+ items: [],
241
+ dynamicCollection: {
242
+ operations: ["add"],
243
+ postUrl: "https://x/cloud-events",
244
+ events: {
245
+ add: [addAction],
246
+ },
247
+ },
248
+ } as any;
249
+
250
+ const refetch = jest.fn().mockResolvedValue(undefined);
251
+ const col = new RemoteEditableCollection(content, refetch);
252
+
253
+ await col.add();
254
+
255
+ expect(handleAction).toHaveBeenCalledWith(addAction);
256
+ expect(refetch).toHaveBeenCalledTimes(1);
257
+ });
258
+ });
259
+ });