@applicaster/zapp-react-native-ui-components 16.0.0-alpha.9235607812 → 16.0.0-alpha.9338540010

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 (33) hide show
  1. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +12 -3
  2. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +1 -1
  3. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +4 -2
  4. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/__tests__/factory.test.ts +2 -2
  5. package/Components/MasterCell/SharedUI/CollapsibleTextContainer/index.ts +2 -1
  6. package/Components/MasterCell/__tests__/masterCell.test.js +60 -0
  7. package/Components/MasterCell/index.tsx +28 -4
  8. package/Components/MasterCell/utils/__tests__/withoutFocusedState.test.js +20 -0
  9. package/Components/MasterCell/utils/index.ts +18 -0
  10. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +22 -1
  11. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +52 -1
  12. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +77 -0
  13. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +58 -16
  14. package/Components/ModalComponent/BottomSheetModalContent.tsx +9 -74
  15. package/Components/ModalComponent/__tests__/showTextInput.test.ts +25 -0
  16. package/Components/ModalComponent/showTextInput.ts +2 -0
  17. package/Components/River/ComponentsMap/ComponentsMap.tsx +4 -1
  18. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +2 -0
  19. package/Components/Transitioner/AnimationManager.js +1 -1
  20. package/Components/Transitioner/Scene.tsx +4 -3
  21. package/Components/Transitioner/Transitioner.tsx +69 -30
  22. package/Components/Transitioner/__tests__/transitioner.test.js +308 -20
  23. package/Components/VideoModal/PlayerWrapper.tsx +4 -1
  24. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -0
  25. package/Components/VideoModal/__tests__/playerWrapperStyle.test.ts +58 -0
  26. package/Components/VideoModal/__tests__/playerWrapperUtils.test.ts +16 -0
  27. package/Components/VideoModal/playerWrapperStyle.ts +21 -3
  28. package/Components/VideoModal/playerWrapperUtils.ts +7 -1
  29. package/Decorators/ZappPipesDataConnector/__tests__/screenToolbox.test.ts +347 -0
  30. package/Decorators/ZappPipesDataConnector/__tests__/screenToolboxUnavailable.test.ts +53 -0
  31. package/Decorators/ZappPipesDataConnector/index.tsx +19 -4
  32. package/Decorators/ZappPipesDataConnector/screenToolbox.ts +384 -0
  33. package/package.json +5 -5
@@ -0,0 +1,347 @@
1
+ import { renderHook } from "@testing-library/react-native";
2
+
3
+ const mockRegister = jest.fn().mockResolvedValue(undefined);
4
+
5
+ // The X-Ray mock (__mocks__/@applicaster/quick-brick-xray) has no toolbox API: give it one.
6
+ require("@applicaster/quick-brick-xray").registerReactToolbox = mockRegister;
7
+
8
+ // Required after the toolbox API is in place: loading them can register the player toolbox.
9
+ const {
10
+ modalStore,
11
+ } = require("@applicaster/zapp-react-native-utils/modalState");
12
+
13
+ const {
14
+ setXrayNavigator,
15
+ } = require("@applicaster/zapp-react-native-utils/navigationUtils/xrayNavigator");
16
+
17
+ const {
18
+ zappHookModalStore,
19
+ } = require("@applicaster/zapp-react-native-ui-components/Contexts/ZappHookModalContext");
20
+
21
+ const mockStore = {
22
+ rivers: { home: { name: "Home", type: "general_content" } },
23
+ plugins: [
24
+ { identifier: "quick-brick-epg-screen", name: "EPG" },
25
+ { identifier: "login-plugin", name: "Login" },
26
+ ],
27
+ };
28
+
29
+ jest.mock("@applicaster/zapp-react-native-redux/AppStore", () => ({
30
+ appStore: { get: (key) => mockStore[key] },
31
+ }));
32
+
33
+ const { useScreenToolboxRecord, getTopPathname } = require("../screenToolbox");
34
+
35
+ const home = { id: "home", name: "Home", type: "general_content" };
36
+
37
+ const stack = {
38
+ "/river/home": { screen: home },
39
+ "/river/epg": {
40
+ screen: { id: "epg", name: "Guide", type: "quick-brick-epg-screen" },
41
+ },
42
+ "/river/tabs": {
43
+ screen: { id: "tabs", name: "Tabs", type: "quick-brick-tabs" },
44
+ nested: { screen: { id: "t1", name: "Tab 1", type: "general_content" } },
45
+ },
46
+ "/river/home/hooks/login": {
47
+ screen: { identifier: "login-plugin", name: "Login" },
48
+ entry: { payload: { id: "e1", title: "One" } },
49
+ },
50
+ };
51
+
52
+ const navigate = (pathname: string, videoModalState = { visible: false }) =>
53
+ setXrayNavigator({
54
+ getPathname: () => pathname,
55
+ getStackForPathname: (route) =>
56
+ stack[route] ? { route, state: stack[route] } : undefined,
57
+ videoModalState,
58
+ } as unknown as QuickBrickAppNavigator);
59
+
60
+ const mount = (
61
+ component,
62
+ index,
63
+ feed?,
64
+ parentId?,
65
+ pathname = "/river/home"
66
+ ) => {
67
+ const hook = renderHook(() =>
68
+ useScreenToolboxRecord(component, "home", index, parentId, pathname)
69
+ );
70
+
71
+ hook.result.current.feed = feed;
72
+
73
+ return hook;
74
+ };
75
+
76
+ const current = (args = {}) => {
77
+ // Other toolboxes (player) may register first.
78
+ const { tools } = mockRegister.mock.calls.find(
79
+ ([toolbox]) => toolbox.name === "screen"
80
+ )[0];
81
+
82
+ return tools.find((tool) => tool.name === "current").handler(args);
83
+ };
84
+
85
+ const presentHook = (overrides = {}) =>
86
+ zappHookModalStore.setState({
87
+ state: {
88
+ path: "/hooks/login",
89
+ screenData: {
90
+ hookPlugin: { identifier: "login-plugin", name: "Login" },
91
+ payload: { id: "e1", title: "One" },
92
+ },
93
+ } as any,
94
+ isHooksExecutionInProgress: true,
95
+ isRunningInBackground: false,
96
+ ...overrides,
97
+ });
98
+
99
+ describe("screenToolbox", () => {
100
+ // Registers the toolbox; the registration is kept for the whole module, as in the app.
101
+ beforeAll(() => mount({ id: "init" }, 0).unmount());
102
+
103
+ beforeEach(() => navigate("/river/home"));
104
+
105
+ afterEach(() => {
106
+ modalStore.getState().setModalState({ visible: false, screen: null });
107
+ zappHookModalStore.getState().resetState();
108
+ zappHookModalStore.getState().setIsHooksExecutionInProgress(false);
109
+ });
110
+
111
+ it("returns the top screen's mounted components in order, with feed and entries", () => {
112
+ const second = mount({ id: "c2", component_type: "grid" }, 1, {
113
+ url: "https://feed/2",
114
+ loading: true,
115
+ });
116
+
117
+ const first = mount({ id: "c1", component_type: "horizontal_list" }, 0, {
118
+ url: "https://feed/1",
119
+ loading: false,
120
+ data: {
121
+ entry: [
122
+ { id: "e1", title: "One" },
123
+ { id: "e2", title: "Two" },
124
+ ],
125
+ },
126
+ });
127
+
128
+ const third = mount({ id: "c3", component_type: "grid" }, 2, {
129
+ url: "https://feed/3",
130
+ error: new Error("404"),
131
+ });
132
+
133
+ expect(current({ limit: 1 })).toEqual({
134
+ pathname: "/river/home",
135
+ screen: {
136
+ kind: "river",
137
+ id: "home",
138
+ name: "Home",
139
+ type: "general_content",
140
+ },
141
+ entry: null,
142
+ rivers: [
143
+ {
144
+ id: "home",
145
+ name: "Home",
146
+ type: "general_content",
147
+ components: [
148
+ {
149
+ index: 0,
150
+ id: "c1",
151
+ type: "horizontal_list",
152
+ feed: { url: "https://feed/1", state: "loaded", count: 2 },
153
+ entries: [{ index: 0, id: "e1", title: "One" }],
154
+ },
155
+ {
156
+ index: 1,
157
+ id: "c2",
158
+ type: "grid",
159
+ feed: { url: "https://feed/2", state: "loading", count: 0 },
160
+ entries: [],
161
+ },
162
+ {
163
+ index: 2,
164
+ id: "c3",
165
+ type: "grid",
166
+ feed: {
167
+ url: "https://feed/3",
168
+ state: "error",
169
+ error: "404",
170
+ count: 0,
171
+ },
172
+ entries: [],
173
+ },
174
+ ],
175
+ },
176
+ ],
177
+ mounted: ["/river/home"],
178
+ });
179
+
180
+ [first, second, third].forEach((hook) => hook.unmount());
181
+
182
+ expect(current().rivers).toEqual([]);
183
+ });
184
+
185
+ it("nests a group's children under it", () => {
186
+ const group = mount({ id: "g", component_type: "group" }, 1);
187
+
188
+ const child = mount(
189
+ { id: "g0", component_type: "hero" },
190
+ 0,
191
+ undefined,
192
+ "g"
193
+ );
194
+
195
+ const row = mount({ id: "r", component_type: "grid" }, 0);
196
+
197
+ expect(
198
+ current().rivers[0].components.map(({ id, children }) => ({
199
+ id,
200
+ children: children?.map((item) => item.id),
201
+ }))
202
+ ).toEqual([
203
+ { id: "r", children: undefined },
204
+ { id: "g", children: ["g0"] },
205
+ ]);
206
+
207
+ [group, child, row].forEach((hook) => hook.unmount());
208
+ });
209
+
210
+ it("reads another mounted screen by pathname", () => {
211
+ const other = mount({ id: "c" }, 0, undefined, undefined, "/river/epg");
212
+
213
+ expect(current({ pathname: "/river/epg" }).rivers[0].components).toEqual([
214
+ expect.objectContaining({ id: "c" }),
215
+ ]);
216
+
217
+ expect(current().mounted).toEqual(["/river/epg"]);
218
+
219
+ other.unmount();
220
+ });
221
+
222
+ it("names the plugin that renders a screen plugin", () => {
223
+ navigate("/river/epg");
224
+
225
+ expect(current().screen).toEqual({
226
+ kind: "plugin",
227
+ id: "epg",
228
+ name: "Guide",
229
+ type: "quick-brick-epg-screen",
230
+ plugin: { identifier: "quick-brick-epg-screen", name: "EPG" },
231
+ });
232
+ });
233
+
234
+ it("reports the nested tab screen", () => {
235
+ navigate("/river/tabs");
236
+
237
+ expect(current().nested).toEqual({
238
+ id: "t1",
239
+ name: "Tab 1",
240
+ type: "general_content",
241
+ });
242
+ });
243
+
244
+ it("unwraps the entry of a hook screen on the stack", () => {
245
+ navigate("/river/home/hooks/login");
246
+
247
+ expect(current()).toMatchObject({
248
+ screen: { kind: "hook", plugin: { identifier: "login-plugin" } },
249
+ entry: { id: "e1", title: "One" },
250
+ });
251
+ });
252
+
253
+ it("says when there is no screen yet", () => {
254
+ navigate("/");
255
+
256
+ expect(current().screen).toEqual({ kind: "none" });
257
+ });
258
+
259
+ it("reads a modal's screen from the modal store, its pathname as ModalProvider builds it", () => {
260
+ modalStore.getState().openModal({ item: home, props: { screenId: "m1" } });
261
+
262
+ expect(current()).toMatchObject({
263
+ pathname: "modal/m1",
264
+ screen: { kind: "river", id: "home" },
265
+ });
266
+ });
267
+
268
+ it("describes a modal hook by its plugin", () => {
269
+ modalStore.getState().openModal({
270
+ item: {
271
+ hookPlugin: { identifier: "login-plugin", name: "Login" },
272
+ payload: { id: "e1", title: "One" },
273
+ },
274
+ props: { screenType: "hooks" },
275
+ } as any);
276
+
277
+ expect(current()).toMatchObject({
278
+ screen: {
279
+ kind: "hook",
280
+ plugin: { identifier: "login-plugin", name: "Login" },
281
+ },
282
+ entry: { id: "e1", title: "One" },
283
+ });
284
+ });
285
+
286
+ it("describes a bottom sheet", () => {
287
+ modalStore.getState().openModal({
288
+ item: { modalBottomSheetContentProps: {} },
289
+ } as any);
290
+
291
+ expect(current().screen).toEqual({ kind: "bottomSheet" });
292
+ });
293
+
294
+ it("puts a modal over the video modal, and the video modal over the stack unless docked", () => {
295
+ const item = { id: "v1", title: "Video" };
296
+
297
+ navigate("/river/home", {
298
+ visible: true,
299
+ mode: "MAXIMIZED",
300
+ item,
301
+ } as any);
302
+
303
+ expect(current()).toMatchObject({
304
+ pathname: "video-modal/v1",
305
+ screen: { kind: "player" },
306
+ entry: { id: "v1", title: "Video" },
307
+ });
308
+
309
+ modalStore.getState().openModal({ item: home });
310
+
311
+ expect(getTopPathname()).toBe("modal/home");
312
+
313
+ modalStore.getState().setModalState({ visible: false, screen: null });
314
+
315
+ navigate("/river/home", {
316
+ visible: true,
317
+ mode: "MINIMIZED",
318
+ item,
319
+ } as any);
320
+
321
+ expect(getTopPathname()).toBe("/river/home");
322
+ });
323
+
324
+ it("puts a presented hook screen on top, not one running in the background", () => {
325
+ presentHook();
326
+
327
+ expect(current()).toMatchObject({
328
+ pathname: "hooks-modal/Login",
329
+ screen: {
330
+ kind: "hook",
331
+ name: "Login",
332
+ plugin: { identifier: "login-plugin" },
333
+ },
334
+ entry: { id: "e1", title: "One" },
335
+ });
336
+
337
+ presentHook({ isRunningInBackground: true });
338
+
339
+ expect(getTopPathname()).toBe("/river/home");
340
+ });
341
+
342
+ it("describes nothing for a modal pathname that is no longer presented", () => {
343
+ expect(current({ pathname: "modal/gone" }).screen).toEqual({
344
+ kind: "none",
345
+ });
346
+ });
347
+ });
@@ -0,0 +1,53 @@
1
+ import { act, renderHook } from "@testing-library/react-native";
2
+
3
+ // Fails the screen toolbox as a build without the X-Ray bridge does. Others (player, loaded with
4
+ // the toolbox's imports) register, or the helper would never let the screen toolbox try.
5
+ const mockRegister = jest.fn((toolbox) =>
6
+ toolbox.name === "screen"
7
+ ? Promise.reject(new Error("no bridge"))
8
+ : Promise.resolve()
9
+ );
10
+
11
+ // The X-Ray mock (__mocks__/@applicaster/quick-brick-xray) has no toolbox API: give it one.
12
+ require("@applicaster/quick-brick-xray").registerReactToolbox = mockRegister;
13
+
14
+ jest.mock("@applicaster/zapp-react-native-redux/AppStore", () => ({
15
+ appStore: { get: () => undefined },
16
+ }));
17
+
18
+ const { useScreenToolboxRecord } = require("../screenToolbox");
19
+
20
+ const mount = (id: string) =>
21
+ renderHook(() =>
22
+ useScreenToolboxRecord({ id }, "home", 0, undefined, "/river/home")
23
+ );
24
+
25
+ const mounted = () => {
26
+ // Other toolboxes (player) may register first.
27
+ const { tools } = mockRegister.mock.calls.find(
28
+ ([toolbox]) => toolbox.name === "screen"
29
+ )[0];
30
+
31
+ return tools
32
+ .find((tool) => tool.name === "current")
33
+ .handler({ pathname: "/river/home" }).mounted;
34
+ };
35
+
36
+ describe("screenToolbox without the X-Ray bridge", () => {
37
+ it("drops its records and stops recording", async () => {
38
+ mount("c1");
39
+
40
+ // Let the failed registration settle.
41
+ await act(async () => {});
42
+
43
+ expect(mounted()).toEqual([]);
44
+
45
+ mount("c2");
46
+
47
+ expect(mounted()).toEqual([]);
48
+
49
+ expect(
50
+ mockRegister.mock.calls.filter(([toolbox]) => toolbox.name === "screen")
51
+ ).toHaveLength(1);
52
+ });
53
+ });
@@ -5,7 +5,9 @@ import { useRoute } from "@applicaster/zapp-react-native-utils/reactHooks/naviga
5
5
  import { usePlugins } from "@applicaster/zapp-react-native-redux/hooks";
6
6
  import { ZappPipesSearchContext } from "@applicaster/zapp-react-native-ui-components/Contexts";
7
7
  import { useScreenContext } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
8
+ import { useUIComponentContext } from "@applicaster/zapp-react-native-ui-components/Contexts/UIComponentContext";
8
9
  import { ResolverSelector } from "./ResolverSelector";
10
+ import { useScreenToolboxRecord } from "./screenToolbox";
9
11
  import { ComponentDataSourceContext } from "./types";
10
12
 
11
13
  type Props = {
@@ -22,7 +24,7 @@ export function zappPipesDataConnector(
22
24
  Component: React.FC<any> | React.ComponentClass<any>
23
25
  ) {
24
26
  return function WrappedWithZappPipesData(props: Props) {
25
- const { screenData } = useRoute();
27
+ const { screenData, pathname } = useRoute();
26
28
  const plugins = usePlugins();
27
29
  const screenContextData = useScreenContext();
28
30
 
@@ -53,6 +55,16 @@ export function zappPipesDataConnector(
53
55
 
54
56
  const [searchContext] = ZappPipesSearchContext.useZappPipesContext();
55
57
 
58
+ const parent = useUIComponentContext() as { id?: string } | null;
59
+
60
+ const toolboxRecord = useScreenToolboxRecord(
61
+ component,
62
+ riverId,
63
+ componentIndex,
64
+ parent?.id,
65
+ pathname
66
+ );
67
+
56
68
  // Prepare context for resolvers
57
69
  const resolverProps: ComponentDataSourceContext = {
58
70
  component,
@@ -71,9 +83,12 @@ export function zappPipesDataConnector(
71
83
 
72
84
  return (
73
85
  <ResolverSelector {...resolverProps}>
74
- {(zappPipesDataProps) => (
75
- <Component {...props} {...zappPipesDataProps} />
76
- )}
86
+ {(zappPipesDataProps) => {
87
+ // A plain field on the toolbox record, read only when the agent asks: no render.
88
+ toolboxRecord.feed = zappPipesDataProps.zappPipesData;
89
+
90
+ return <Component {...props} {...zappPipesDataProps} />;
91
+ }}
77
92
  </ResolverSelector>
78
93
  );
79
94
  };