@applicaster/zapp-react-native-utils 16.0.0-alpha.1701179921 → 16.0.0-alpha.1733304298
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/actionUtils/index.ts +3 -1
- package/actionsExecutor/__tests__/StorageActions.test.ts +98 -0
- package/actionsExecutor/__tests__/feedDecorator.test.ts +30 -0
- package/actionsExecutor/__tests__/screenToggleFlag.test.ts +90 -0
- package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +139 -5
- package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +24 -0
- package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +72 -0
- package/actionsExecutor/actions/navigateToScreen.ts +29 -45
- package/actionsExecutor/actions/openBottomSheet.ts +2 -0
- package/actionsExecutor/actions/screenToggleFlag.ts +1 -3
- package/actionsExecutor/feedDecorator.ts +6 -0
- package/analyticsUtils/__tests__/playerAnalyticsSafeguard.test.ts +62 -0
- package/analyticsUtils/playerAnalyticsTracker.ts +1 -0
- package/appUtils/contextKeysManager/__tests__/getKey/failure.test.ts +1 -22
- package/appUtils/contextKeysManager/__tests__/getKey/success/fullSearch.test.ts +1 -25
- package/appUtils/contextKeysManager/__tests__/getKey/success/searchInSecure.test.ts +1 -7
- package/appUtils/contextKeysManager/__tests__/removeKey/failure.test.ts +0 -25
- package/appUtils/contextKeysManager/__tests__/removeKey/success.test.ts +0 -6
- package/appUtils/contextKeysManager/__tests__/setKey/failure/failedToSaveInStorage.test.ts +0 -40
- package/appUtils/contextKeysManager/__tests__/setKey/{success/saveItemAndReference.test.ts → success.test.ts} +2 -42
- package/appUtils/contextKeysManager/index.ts +28 -129
- package/appUtils/playerManager/__tests__/playerDiagnostics.test.ts +62 -0
- package/appUtils/playerManager/__tests__/playerToolbox.test.ts +281 -0
- package/appUtils/playerManager/__tests__/usePlayerControllerSetup.test.tsx +62 -0
- package/appUtils/playerManager/index.ts +7 -0
- package/appUtils/playerManager/player.ts +15 -0
- package/appUtils/playerManager/playerToolbox.ts +196 -0
- package/appUtils/playerManager/usePlayerControllerSetup.tsx +6 -0
- package/cellUtils/__tests__/getButtonsEnabled.test.ts +28 -0
- package/cellUtils/__tests__/getCellNavigationEnabled.test.ts +36 -0
- package/cellUtils/__tests__/getIsCellNavigationEnabled.test.ts +35 -0
- package/cellUtils/__tests__/isCellNavigationEnabled.test.ts +21 -0
- package/cellUtils/index.ts +40 -0
- package/configurationUtils/__tests__/modalBlocksParser.test.ts +134 -0
- package/configurationUtils/modalBlocksParser.ts +38 -0
- package/focusManager/FocusManager.ts +26 -0
- package/focusManager/__tests__/FocusManager.test.ts +9 -0
- package/localizationUtils/index.ts +12 -6
- package/logger/__tests__/xrayToolbox.test.ts +69 -0
- package/logger/xrayToolbox.ts +67 -0
- package/manifestUtils/defaultManifestConfigurations/player.js +8 -0
- package/manifestUtils/keys.js +7 -2
- package/manifestUtils/mobileAction/container/__tests__/mobileActionButtonsContainer.test.js +21 -0
- package/manifestUtils/mobileAction/groups/__tests__/buildMobileActionButtonGroups.test.js +1 -1
- package/manifestUtils/mobileAction/groups/defaults.js +2 -1
- package/modalState/ModalOrchestrator.ts +1 -0
- package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +23 -0
- package/modalState/index.ts +2 -0
- package/modalState/types.ts +11 -0
- package/navigationUtils/xrayNavigator.ts +12 -0
- package/package.json +3 -2
- package/quickBrickAssets/__tests__/index.test.ts +18 -0
- package/quickBrickAssets/index.ts +35 -0
- package/reactHooks/__tests__/getPluginConfiguration.test.ts +82 -0
- package/reactHooks/actions/index.ts +29 -23
- package/reactHooks/connection/index.tsx +8 -5
- package/reactHooks/feed/__tests__/feedInvalidation.test.tsx +338 -0
- package/reactHooks/feed/__tests__/useBatchLoading.test.tsx +8 -2
- package/reactHooks/feed/__tests__/useMarkPipesDataStale.test.tsx +243 -0
- package/reactHooks/feed/useFeedLoader.tsx +28 -1
- package/reactHooks/feed/useLoadPipesDataDispatch.ts +12 -11
- package/reactHooks/feed/useMarkPipesDataStale.ts +68 -23
- package/reactHooks/usePluginConfiguration.ts +84 -46
- package/riverComponetsMeasurementProvider/index.tsx +3 -1
- package/uiActionsRegistrator/__tests__/subscribe.test.ts +26 -0
- package/uiActionsRegistrator/registry.ts +16 -0
- package/appUtils/contextKeysManager/__tests__/getKey/success/byReference.test.ts +0 -56
- package/appUtils/contextKeysManager/__tests__/getKeyByReference/failure.test.ts +0 -265
- package/appUtils/contextKeysManager/__tests__/getKeyByReference/success.test.ts +0 -303
- package/appUtils/contextKeysManager/__tests__/removeReferenceForKey/failure.test.ts +0 -82
- package/appUtils/contextKeysManager/__tests__/removeReferenceForKey/success.test.ts +0 -41
- package/appUtils/contextKeysManager/__tests__/setKey/success/saveItemButNotReference.test.ts +0 -208
- package/appUtils/contextKeysManager/__tests__/setReferenceForKey/failure.test.ts +0 -93
- package/appUtils/contextKeysManager/__tests__/setReferenceForKey/success.test.ts +0 -45
package/actionUtils/index.ts
CHANGED
|
@@ -3,5 +3,7 @@ import { isTV } from "../reactUtils";
|
|
|
3
3
|
|
|
4
4
|
export const resolveDefaultAssetUri = (source: string | number): string =>
|
|
5
5
|
isTV()
|
|
6
|
-
?
|
|
6
|
+
? typeof source === "number"
|
|
7
|
+
? Image.resolveAssetSource(source)?.uri || ""
|
|
8
|
+
: (source as string)
|
|
7
9
|
: Image.resolveAssetSource(source as number)?.uri || "";
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { ActionResult } from "../ActionExecutor";
|
|
2
|
+
import { localStorageToggleFlag } from "../StorageActions";
|
|
3
|
+
import { TOGGLE_FLAG_MAX_ITEMS_REACHED_EVENT } from "../consts";
|
|
4
|
+
import { postEvent } from "../../reactHooks/useSubscriberFor";
|
|
5
|
+
|
|
6
|
+
jest.mock("../ActionExecutorContext", () => ({
|
|
7
|
+
log_error: jest.fn(),
|
|
8
|
+
log_info: jest.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
jest.mock("../../reactHooks/useSubscriberFor", () => ({
|
|
12
|
+
postEvent: jest.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
// In-memory stand-in for the storage-backed provider; the limit check under
|
|
16
|
+
// test lives in storageToggleFlag, not in the provider.
|
|
17
|
+
const mockStore: Record<string, string[]> = {};
|
|
18
|
+
|
|
19
|
+
jest.mock("../../storage/StorageMultiSelectProvider", () => ({
|
|
20
|
+
StorageMultiSelectProvider: {
|
|
21
|
+
getProvider: (keyNamespace: string) => ({
|
|
22
|
+
getSelectedAsync: async () => [...(mockStore[keyNamespace] ?? [])],
|
|
23
|
+
addItem: async (item: string) => {
|
|
24
|
+
mockStore[keyNamespace] = [...(mockStore[keyNamespace] ?? []), item];
|
|
25
|
+
},
|
|
26
|
+
removeItem: async (item: string) => {
|
|
27
|
+
mockStore[keyNamespace] = (mockStore[keyNamespace] ?? []).filter(
|
|
28
|
+
(it) => it !== item
|
|
29
|
+
);
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
const KEY = "user_preferences.teams";
|
|
36
|
+
|
|
37
|
+
// Entries of a team selection feed with explicit tap actions
|
|
38
|
+
// (localStorageToggleFlag with key and selector).
|
|
39
|
+
const teamEntry = (n: number) =>
|
|
40
|
+
({
|
|
41
|
+
id: `team-${n}`,
|
|
42
|
+
title: `Team ${n}`,
|
|
43
|
+
type: { value: "action" },
|
|
44
|
+
extensions: { tag: `TEAM ${n}` },
|
|
45
|
+
}) as unknown as ZappEntry;
|
|
46
|
+
|
|
47
|
+
const tapAction = (extraOptions = {}) =>
|
|
48
|
+
({
|
|
49
|
+
type: "localStorageToggleFlag",
|
|
50
|
+
options: { key: KEY, selector: "extensions.tag", ...extraOptions },
|
|
51
|
+
}) as unknown as ActionType;
|
|
52
|
+
|
|
53
|
+
const tapTeams = async (count: number, extraOptions = {}) => {
|
|
54
|
+
const results: ActionResult[] = [];
|
|
55
|
+
|
|
56
|
+
for (let n = 1; n <= count; n++) {
|
|
57
|
+
results.push(
|
|
58
|
+
await localStorageToggleFlag(
|
|
59
|
+
{ entry: teamEntry(n) },
|
|
60
|
+
tapAction(extraOptions)
|
|
61
|
+
)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return results;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
describe("localStorageToggleFlag max_items", () => {
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
delete mockStore[KEY];
|
|
71
|
+
jest.clearAllMocks();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("blocks the 11th team when max_items is in the action options", async () => {
|
|
75
|
+
const results = await tapTeams(11, { max_items: "10" });
|
|
76
|
+
|
|
77
|
+
expect(results.slice(0, 10)).toEqual(Array(10).fill(ActionResult.Success));
|
|
78
|
+
expect(results[10]).toBe(ActionResult.Cancel);
|
|
79
|
+
expect(mockStore[KEY]).toHaveLength(10);
|
|
80
|
+
|
|
81
|
+
expect(postEvent).toHaveBeenCalledWith(
|
|
82
|
+
TOGGLE_FLAG_MAX_ITEMS_REACHED_EVENT,
|
|
83
|
+
[expect.objectContaining({ maxItems: "10", tag: "TEAM 11" })]
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("still lets a selected team be removed at the limit", async () => {
|
|
88
|
+
await tapTeams(10, { max_items: "10" });
|
|
89
|
+
|
|
90
|
+
const result = await localStorageToggleFlag(
|
|
91
|
+
{ entry: teamEntry(1) },
|
|
92
|
+
tapAction({ max_items: "10" })
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(result).toBe(ActionResult.Success);
|
|
96
|
+
expect(mockStore[KEY]).toHaveLength(9);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -59,3 +59,33 @@ describe("decorateFeed", () => {
|
|
|
59
59
|
expect(decorated.entry[0].extensions.tap_actions).toBeUndefined();
|
|
60
60
|
});
|
|
61
61
|
});
|
|
62
|
+
|
|
63
|
+
describe("decorateFeed max_items", () => {
|
|
64
|
+
const toggleAction = (feed: ZappFeed) =>
|
|
65
|
+
decorateFeed(feed).entry[0].extensions.tap_actions.actions[0];
|
|
66
|
+
|
|
67
|
+
it.each([
|
|
68
|
+
["screen", "screenToggleFlag"],
|
|
69
|
+
["session", "sessionStorageToggleFlag"],
|
|
70
|
+
["ctx", "localStorageToggleFlag"],
|
|
71
|
+
])("passes max_items to the %s toggle action", (scope, type) => {
|
|
72
|
+
const feed = preferenceFeed("multi");
|
|
73
|
+
feed.extensions.preference_editor_options.scope = scope;
|
|
74
|
+
feed.extensions.preference_editor_options.max_items = "10";
|
|
75
|
+
|
|
76
|
+
const toggle = toggleAction(feed);
|
|
77
|
+
|
|
78
|
+
expect(toggle.type).toBe(type);
|
|
79
|
+
expect(toggle.options).toEqual({ key: "agree", max_items: 10 });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it.each([undefined, 0, "0", "ten", -1])(
|
|
83
|
+
"does not set a limit for max_items %p",
|
|
84
|
+
(maxItems) => {
|
|
85
|
+
const feed = preferenceFeed("multi");
|
|
86
|
+
feed.extensions.preference_editor_options.max_items = maxItems;
|
|
87
|
+
|
|
88
|
+
expect(toggleAction(feed).options).toEqual({ key: "agree" });
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { ActionExecutor, ActionResult } from "../ActionExecutor";
|
|
2
|
+
import { screenToggleFlagAction } from "../actions/screenToggleFlag";
|
|
3
|
+
|
|
4
|
+
jest.mock("../ActionExecutorContext", () => ({
|
|
5
|
+
log_error: jest.fn(),
|
|
6
|
+
log_info: jest.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
jest.mock("../StorageActions", () => ({
|
|
10
|
+
onMaxTagsReached: jest.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// not used here, but it pulls the native bridge logger in at import time
|
|
14
|
+
jest.mock("../../storage/ScreenSingleValueProvider", () => ({}));
|
|
15
|
+
|
|
16
|
+
jest.mock("../../logger", () => ({
|
|
17
|
+
createLogger: () => ({
|
|
18
|
+
log_error: jest.fn(),
|
|
19
|
+
log_debug: jest.fn(),
|
|
20
|
+
log_info: jest.fn(),
|
|
21
|
+
}),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const mockSelected: string[] = [];
|
|
25
|
+
|
|
26
|
+
jest.mock("../../storage/ScreenStateMultiSelectProvider", () => ({
|
|
27
|
+
ScreenMultiSelectProvider: {
|
|
28
|
+
getProvider: () => ({
|
|
29
|
+
getSelectedAsync: async () => [...mockSelected],
|
|
30
|
+
addItem: async (item: string) => {
|
|
31
|
+
mockSelected.push(item);
|
|
32
|
+
},
|
|
33
|
+
removeItem: async (item: string) => {
|
|
34
|
+
mockSelected.splice(mockSelected.indexOf(item), 1);
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const toggleAction = (options = {}) =>
|
|
41
|
+
({
|
|
42
|
+
type: "screenToggleFlag",
|
|
43
|
+
options: { key: "teams", selector: "extensions.tag", ...options },
|
|
44
|
+
}) as unknown as ActionType;
|
|
45
|
+
|
|
46
|
+
const context = (tag: string) => ({
|
|
47
|
+
screenRoute: "/screen",
|
|
48
|
+
screenStateStore: {},
|
|
49
|
+
entry: { id: tag, extensions: { tag } },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("screenToggleFlagAction at the max_items limit", () => {
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
mockSelected.length = 0;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns the cancellation so the chained actions do not run", async () => {
|
|
58
|
+
const afterToggle = jest.fn();
|
|
59
|
+
const executor = new ActionExecutor();
|
|
60
|
+
|
|
61
|
+
executor.registerAction("screenToggleFlag", screenToggleFlagAction);
|
|
62
|
+
|
|
63
|
+
executor.registerAction("sendCloudEvent", async () => {
|
|
64
|
+
afterToggle();
|
|
65
|
+
|
|
66
|
+
return ActionResult.Success;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
mockSelected.push("A", "B");
|
|
70
|
+
|
|
71
|
+
const result = await executor.handleActions(
|
|
72
|
+
[toggleAction({ max_items: 2 }), { type: "sendCloudEvent" }],
|
|
73
|
+
context("C") as never
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(result).toBe(ActionResult.Cancel);
|
|
77
|
+
expect(afterToggle).not.toHaveBeenCalled();
|
|
78
|
+
expect(mockSelected).toEqual(["A", "B"]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("lets the chain continue below the limit", async () => {
|
|
82
|
+
const result = await screenToggleFlagAction(
|
|
83
|
+
toggleAction({ max_items: 2 }),
|
|
84
|
+
context("A") as never
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(result).toBe(ActionResult.Success);
|
|
88
|
+
expect(mockSelected).toEqual(["A"]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -83,9 +83,7 @@ describe("navigateToScreen navigationOptions", () => {
|
|
|
83
83
|
);
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
// non-push path falls back to replaceTop().
|
|
88
|
-
it("forwards navigationOptions to replaceTop when navigating with an entry", async () => {
|
|
86
|
+
it("forwards navigationOptions to replace when navigating with an entry", async () => {
|
|
89
87
|
const navigator = createNavigator();
|
|
90
88
|
const entry = { id: "entry-id" } as unknown as ZappEntry;
|
|
91
89
|
|
|
@@ -100,12 +98,12 @@ describe("navigateToScreen navigationOptions", () => {
|
|
|
100
98
|
|
|
101
99
|
await createNavigateToScreenAction(navigator, rivers, contentTypes)(action);
|
|
102
100
|
|
|
103
|
-
expect(navigator.
|
|
101
|
+
expect(navigator.replace).toHaveBeenCalledWith(
|
|
104
102
|
{ id: "entry-id", type: { value: "parentLock" } },
|
|
105
103
|
{ pinFlow: "change-pin" }
|
|
106
104
|
);
|
|
107
105
|
|
|
108
|
-
expect(navigator.
|
|
106
|
+
expect(navigator.replaceTop).not.toHaveBeenCalled();
|
|
109
107
|
});
|
|
110
108
|
|
|
111
109
|
// Regression guard: the no-options path must keep working unchanged.
|
|
@@ -131,3 +129,139 @@ describe("navigateToScreen navigationOptions", () => {
|
|
|
131
129
|
expect(result).toBe(ActionResult.Success);
|
|
132
130
|
});
|
|
133
131
|
});
|
|
132
|
+
|
|
133
|
+
describe("navigateToScreen target resolution", () => {
|
|
134
|
+
function run(options, context?) {
|
|
135
|
+
const navigator = createNavigator();
|
|
136
|
+
|
|
137
|
+
const result = createNavigateToScreenAction(
|
|
138
|
+
navigator,
|
|
139
|
+
rivers,
|
|
140
|
+
contentTypes
|
|
141
|
+
)({ type: "navigateToScreen", options }, context);
|
|
142
|
+
|
|
143
|
+
return { navigator, result };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
it("overrides entry.type with typeMapping when both are provided", async () => {
|
|
147
|
+
const entry = {
|
|
148
|
+
id: "entry-id",
|
|
149
|
+
type: { value: "video" },
|
|
150
|
+
} as unknown as ZappEntry;
|
|
151
|
+
|
|
152
|
+
const { navigator, result } = run({
|
|
153
|
+
typeMapping: "parentLock",
|
|
154
|
+
navigationAction: "push",
|
|
155
|
+
entry,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(await result).toBe(ActionResult.Success);
|
|
159
|
+
|
|
160
|
+
expect(navigator.push).toHaveBeenCalledWith(
|
|
161
|
+
{ id: "entry-id", type: { value: "parentLock" } },
|
|
162
|
+
undefined
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expect(entry.type).toEqual({ value: "video" });
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("navigates with the entry's own type when typeMapping is not provided", async () => {
|
|
169
|
+
const entry = {
|
|
170
|
+
id: "entry-id",
|
|
171
|
+
type: { value: "video" },
|
|
172
|
+
} as unknown as ZappEntry;
|
|
173
|
+
|
|
174
|
+
const { navigator, result } = run({ navigationAction: "push", entry });
|
|
175
|
+
|
|
176
|
+
expect(await result).toBe(ActionResult.Success);
|
|
177
|
+
|
|
178
|
+
expect(navigator.push).toHaveBeenCalledWith(
|
|
179
|
+
{ id: "entry-id", type: { value: "video" } },
|
|
180
|
+
undefined
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("resolves the context entry when entry is @{entry/} and typeMapping is not provided", async () => {
|
|
185
|
+
const entry = {
|
|
186
|
+
id: "context-entry-id",
|
|
187
|
+
type: { value: "video" },
|
|
188
|
+
} as unknown as ZappEntry;
|
|
189
|
+
|
|
190
|
+
const { navigator, result } = run({ entry: "@{entry/}" }, { entry });
|
|
191
|
+
|
|
192
|
+
expect(await result).toBe(ActionResult.Success);
|
|
193
|
+
|
|
194
|
+
expect(navigator.replace).toHaveBeenCalledWith(
|
|
195
|
+
{ id: "context-entry-id", type: { value: "video" } },
|
|
196
|
+
undefined
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("returns an error when neither typeMapping nor entry type is provided", async () => {
|
|
201
|
+
const entry = { id: "entry-id" } as unknown as ZappEntry;
|
|
202
|
+
|
|
203
|
+
const { navigator, result } = run({ navigationAction: "push", entry });
|
|
204
|
+
|
|
205
|
+
expect(await result).toBe(ActionResult.Error);
|
|
206
|
+
expect(navigator.push).not.toHaveBeenCalled();
|
|
207
|
+
expect(navigator.replace).not.toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("returns an error when neither typeMapping nor entry is provided", async () => {
|
|
211
|
+
const { navigator, result } = run({ navigationAction: "push" });
|
|
212
|
+
|
|
213
|
+
expect(await result).toBe(ActionResult.Error);
|
|
214
|
+
expect(navigator.push).not.toHaveBeenCalled();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("returns an error when typeMapping has no screen_id mapping", async () => {
|
|
218
|
+
const callback = jest.fn();
|
|
219
|
+
|
|
220
|
+
const { navigator, result } = run(
|
|
221
|
+
{ typeMapping: "unknownType", navigationAction: "push" },
|
|
222
|
+
{ callback }
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
expect(await result).toBe(ActionResult.Error);
|
|
226
|
+
expect(navigator.push).not.toHaveBeenCalled();
|
|
227
|
+
expect(callback).not.toHaveBeenCalled();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("returns an error when the mapped screen_id has no river", async () => {
|
|
231
|
+
const navigator = createNavigator();
|
|
232
|
+
const callback = jest.fn();
|
|
233
|
+
|
|
234
|
+
const result = await createNavigateToScreenAction(navigator, rivers, {
|
|
235
|
+
orphan: { screen_id: "missing-screen-id" },
|
|
236
|
+
} as unknown as ZappContentTypes)(
|
|
237
|
+
{
|
|
238
|
+
type: "navigateToScreen",
|
|
239
|
+
options: { typeMapping: "orphan", navigationAction: "push" },
|
|
240
|
+
},
|
|
241
|
+
{ callback }
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
expect(result).toBe(ActionResult.Error);
|
|
245
|
+
expect(navigator.push).not.toHaveBeenCalled();
|
|
246
|
+
expect(callback).not.toHaveBeenCalled();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("aborts the action pipeline before navigating to a resolved river", async () => {
|
|
250
|
+
const callback = jest.fn();
|
|
251
|
+
|
|
252
|
+
const { navigator, result } = run(
|
|
253
|
+
{ typeMapping: "parentLock" },
|
|
254
|
+
{ callback }
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
expect(await result).toBe(ActionResult.Success);
|
|
258
|
+
|
|
259
|
+
expect(callback).toHaveBeenCalledWith({
|
|
260
|
+
success: false,
|
|
261
|
+
error: null,
|
|
262
|
+
abort: true,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
expect(navigator.replace).toHaveBeenCalledWith(river, undefined);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
@@ -60,6 +60,30 @@ describe("openBottomSheetAction custom content", () => {
|
|
|
60
60
|
});
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
it("passes themePluginId through to the name sheet", async () => {
|
|
64
|
+
const CustomContent = function CustomContent() {
|
|
65
|
+
return null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
await openBottomSheetAction({
|
|
69
|
+
type: "openBottomSheet",
|
|
70
|
+
options: {
|
|
71
|
+
header: { title: "Create New Playlist" },
|
|
72
|
+
themePluginId: "playlist-experience",
|
|
73
|
+
content: {
|
|
74
|
+
component: CustomContent,
|
|
75
|
+
props: { buttonLabel: "Create" },
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
} as any);
|
|
79
|
+
|
|
80
|
+
const args = (openBottomSheetModal as jest.Mock).mock.calls[0][0];
|
|
81
|
+
|
|
82
|
+
expect(args.modalBottomSheetContentProps.themePluginId).toBe(
|
|
83
|
+
"playlist-experience"
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
63
87
|
it("returns Error when neither list data nor content.component is provided", async () => {
|
|
64
88
|
const result = await openBottomSheetAction({
|
|
65
89
|
type: "openBottomSheet",
|
|
@@ -2,6 +2,22 @@ import { openBottomSheetAction } from "../openBottomSheet";
|
|
|
2
2
|
import { actionExecutor, ActionResult } from "../../ActionExecutor";
|
|
3
3
|
import { modalOrchestrator } from "../../../modalState/ModalOrchestrator";
|
|
4
4
|
|
|
5
|
+
jest.mock("@applicaster/zapp-pipes-v2-client", () => ({
|
|
6
|
+
PipesClientResponseHelper: jest.fn(() => ({ error: null })),
|
|
7
|
+
RequestBuilder: jest.fn(() => ({
|
|
8
|
+
setEntryContext: jest.fn().mockReturnThis(),
|
|
9
|
+
setScreenContext: jest.fn().mockReturnThis(),
|
|
10
|
+
setUrl: jest.fn().mockReturnThis(),
|
|
11
|
+
buildAxiosRequest: jest.fn().mockResolvedValue(undefined),
|
|
12
|
+
call: jest.fn().mockResolvedValue({
|
|
13
|
+
response: {
|
|
14
|
+
entry: [],
|
|
15
|
+
extensions: { dynamic_collection_options: { operations: "add" } },
|
|
16
|
+
},
|
|
17
|
+
}),
|
|
18
|
+
})),
|
|
19
|
+
}));
|
|
20
|
+
|
|
5
21
|
jest.mock("../../../modalState/store", () => ({
|
|
6
22
|
openBottomSheetModal: jest.fn(),
|
|
7
23
|
}));
|
|
@@ -165,6 +181,62 @@ describe("openBottomSheetAction inline items", () => {
|
|
|
165
181
|
expect(menu.content.emptyMessage).toBe("Queued items will be added here");
|
|
166
182
|
});
|
|
167
183
|
|
|
184
|
+
it("forwards content.operationButtons onto the menu", async () => {
|
|
185
|
+
const result = await openBottomSheetAction({
|
|
186
|
+
type: "openBottomSheet",
|
|
187
|
+
options: {
|
|
188
|
+
header: { title: "Playlists" },
|
|
189
|
+
content: {
|
|
190
|
+
items: [{ id: "p1", title: "Playlist 1" }],
|
|
191
|
+
operationButtons: {
|
|
192
|
+
add: {
|
|
193
|
+
title: "Create New Playlist",
|
|
194
|
+
iconURL: "https://example.com/add.png",
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
} as any);
|
|
200
|
+
|
|
201
|
+
expect(result).toBe(ActionResult.Success);
|
|
202
|
+
|
|
203
|
+
const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
|
|
204
|
+
|
|
205
|
+
expect(menu.content.operationButtons).toEqual({
|
|
206
|
+
add: {
|
|
207
|
+
title: "Create New Playlist",
|
|
208
|
+
iconURL: "https://example.com/add.png",
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("preserves Add presentation when loading itemsUrl content", async () => {
|
|
214
|
+
const result = await openBottomSheetAction({
|
|
215
|
+
type: "openBottomSheet",
|
|
216
|
+
options: {
|
|
217
|
+
content: {
|
|
218
|
+
itemsUrl: "https://example.com/collections",
|
|
219
|
+
operationButtons: {
|
|
220
|
+
add: {
|
|
221
|
+
title: "Create Collection",
|
|
222
|
+
iconURL: "file:///bundled-add.png",
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(result).toBe(ActionResult.Success);
|
|
230
|
+
const [menu] = (modalOrchestrator.open as jest.Mock).mock.calls[0];
|
|
231
|
+
|
|
232
|
+
expect(menu.content).toMatchObject({
|
|
233
|
+
operationButtons: {
|
|
234
|
+
add: { title: "Create Collection", iconURL: "file:///bundled-add.png" },
|
|
235
|
+
},
|
|
236
|
+
dynamicCollection: { operations: ["add"] },
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
168
240
|
it("pushes a new sheet by default even when another sheet is already open", async () => {
|
|
169
241
|
(modalOrchestrator as any).stackSize = 1;
|
|
170
242
|
|
|
@@ -13,7 +13,7 @@ const { log_info, log_error } = createLogger({
|
|
|
13
13
|
*/
|
|
14
14
|
export interface NavigateToScreenOptions {
|
|
15
15
|
/** The screen type to navigate to (from content types mapping) */
|
|
16
|
-
typeMapping
|
|
16
|
+
typeMapping?: string;
|
|
17
17
|
|
|
18
18
|
/** Navigation action: push adds to history, replace replaces current screen */
|
|
19
19
|
navigationAction?: "push" | "replace";
|
|
@@ -58,14 +58,6 @@ export function createNavigateToScreenAction(
|
|
|
58
58
|
action,
|
|
59
59
|
context?: ActionExecutionContext
|
|
60
60
|
): Promise<ActionResult> {
|
|
61
|
-
const screenType = action.options?.typeMapping;
|
|
62
|
-
|
|
63
|
-
if (!screenType) {
|
|
64
|
-
log_error("navigateToScreen: typeMapping option is missing");
|
|
65
|
-
|
|
66
|
-
return ActionResult.Error;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
61
|
const navigationAction = action.options?.navigationAction;
|
|
70
62
|
const entrySource = action.options?.entry;
|
|
71
63
|
const navigationOptions = action.options?.navigationOptions;
|
|
@@ -76,61 +68,53 @@ export function createNavigateToScreenAction(
|
|
|
76
68
|
: entrySource
|
|
77
69
|
: null;
|
|
78
70
|
|
|
79
|
-
|
|
80
|
-
if (typeof entry !== "object") {
|
|
81
|
-
log_error(
|
|
82
|
-
`navigateToScreen: entry option is not an object, entry: ${entry}`
|
|
83
|
-
);
|
|
71
|
+
const screenType = action.options?.typeMapping || entry?.type?.value;
|
|
84
72
|
|
|
85
|
-
|
|
86
|
-
|
|
73
|
+
const navigate =
|
|
74
|
+
navigationAction === "push" ? navigator.push : navigator.replace;
|
|
75
|
+
|
|
76
|
+
let navigatingEntry: ZappEntry | ZappRiver = entry;
|
|
87
77
|
|
|
78
|
+
if (entry && screenType) {
|
|
88
79
|
log_info(
|
|
89
80
|
`navigateToScreen: navigating to screen type: ${screenType} with entry id: ${entry.id}`
|
|
90
81
|
);
|
|
91
82
|
|
|
92
|
-
|
|
83
|
+
navigatingEntry = {
|
|
93
84
|
...entry,
|
|
94
|
-
type: {
|
|
95
|
-
value: screenType,
|
|
96
|
-
},
|
|
85
|
+
type: { value: screenType },
|
|
97
86
|
};
|
|
87
|
+
} else {
|
|
88
|
+
if (!screenType) {
|
|
89
|
+
log_error(
|
|
90
|
+
"navigateToScreen: options.typeMapping or entry?.type?.value is not provided"
|
|
91
|
+
);
|
|
98
92
|
|
|
99
|
-
|
|
100
|
-
navigator.push(overriddenEntry, navigationOptions);
|
|
101
|
-
} else {
|
|
102
|
-
// Use replaceTop for entries since replace() only accepts ZappRiver
|
|
103
|
-
navigator.replaceTop(overriddenEntry, navigationOptions);
|
|
93
|
+
return ActionResult.Error;
|
|
104
94
|
}
|
|
105
95
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const screenId = contentTypes?.[screenType]?.screen_id || null;
|
|
96
|
+
const screenId = contentTypes?.[screenType]?.screen_id || null;
|
|
97
|
+
const river = rivers?.[screenId];
|
|
110
98
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
99
|
+
if (!screenId) {
|
|
100
|
+
log_error(
|
|
101
|
+
`navigateToScreen: can not resolve screen type mapping: ${screenType}`
|
|
102
|
+
);
|
|
115
103
|
|
|
116
|
-
|
|
117
|
-
|
|
104
|
+
return ActionResult.Error;
|
|
105
|
+
}
|
|
118
106
|
|
|
119
|
-
|
|
107
|
+
if (!river) {
|
|
108
|
+
log_error("navigateToScreen: can not resolve river");
|
|
120
109
|
|
|
121
|
-
|
|
122
|
-
|
|
110
|
+
return ActionResult.Error;
|
|
111
|
+
}
|
|
123
112
|
|
|
124
|
-
|
|
113
|
+
navigatingEntry = river;
|
|
125
114
|
}
|
|
126
115
|
|
|
127
116
|
context?.callback?.({ success: false, error: null, abort: true });
|
|
128
|
-
|
|
129
|
-
if (navigationAction === "push") {
|
|
130
|
-
navigator.push(river, navigationOptions);
|
|
131
|
-
} else {
|
|
132
|
-
navigator.replace(river, navigationOptions);
|
|
133
|
-
}
|
|
117
|
+
navigate(navigatingEntry, navigationOptions);
|
|
134
118
|
|
|
135
119
|
return ActionResult.Success;
|
|
136
120
|
};
|
|
@@ -212,6 +212,7 @@ export async function openBottomSheetAction(
|
|
|
212
212
|
...(content.props || {}),
|
|
213
213
|
actionContext: context,
|
|
214
214
|
},
|
|
215
|
+
themePluginId,
|
|
215
216
|
},
|
|
216
217
|
});
|
|
217
218
|
|
|
@@ -289,6 +290,7 @@ export async function openBottomSheetAction(
|
|
|
289
290
|
themePluginId,
|
|
290
291
|
styleOverrides: styleOverrides ?? content?.styleOverrides,
|
|
291
292
|
emptyMessage: content?.emptyMessage,
|
|
293
|
+
operationButtons: content?.operationButtons,
|
|
292
294
|
context,
|
|
293
295
|
},
|
|
294
296
|
};
|
|
@@ -27,12 +27,10 @@ export const screenToggleFlagAction: ActionHandler<
|
|
|
27
27
|
const screenRoute = context?.screenRoute;
|
|
28
28
|
const screenStateStore = context?.screenStateStore;
|
|
29
29
|
|
|
30
|
-
await screenToggleFlag(
|
|
30
|
+
return await screenToggleFlag(
|
|
31
31
|
screenRoute,
|
|
32
32
|
screenStateStore,
|
|
33
33
|
{ entry: context?.entry, options: action.options },
|
|
34
34
|
action
|
|
35
35
|
);
|
|
36
|
-
|
|
37
|
-
return ActionResult.Success;
|
|
38
36
|
};
|
|
@@ -47,6 +47,11 @@ function makeMultiSelect(feed: ZappFeed, key, decoratedFeed) {
|
|
|
47
47
|
? "sessionStorageToggleFlag"
|
|
48
48
|
: "localStorageToggleFlag";
|
|
49
49
|
|
|
50
|
+
// the toggle actions read their limit from their own options
|
|
51
|
+
const maxItems = Number(
|
|
52
|
+
decoratedFeed.extensions?.preference_editor_options?.max_items
|
|
53
|
+
);
|
|
54
|
+
|
|
50
55
|
decoratedFeed.entry?.forEach((entry: ZappEntry) => {
|
|
51
56
|
entry.type.value = "action";
|
|
52
57
|
entry.id = entry.extensions?.tag || entry.id;
|
|
@@ -59,6 +64,7 @@ function makeMultiSelect(feed: ZappFeed, key, decoratedFeed) {
|
|
|
59
64
|
type: actionType,
|
|
60
65
|
options: {
|
|
61
66
|
key,
|
|
67
|
+
...(maxItems > 0 && { max_items: maxItems }),
|
|
62
68
|
},
|
|
63
69
|
},
|
|
64
70
|
],
|