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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +13 -0
  2. package/Components/MasterCell/DefaultComponents/LiveImage/index.tsx +12 -3
  3. package/Components/MasterCell/DefaultComponents/PressableView.tsx +3 -0
  4. package/Components/MasterCell/DefaultComponents/__tests__/ActionButton.test.tsx +288 -0
  5. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/ActionButton.tsx +2 -1
  6. package/Components/MasterCell/DefaultComponents/mobile/MobileActionButtons/__tests__/PressableView.test.tsx +4 -2
  7. package/Components/MasterCell/__tests__/masterCell.test.js +60 -0
  8. package/Components/MasterCell/index.tsx +28 -4
  9. package/Components/MasterCell/utils/__tests__/withoutFocusedState.test.js +20 -0
  10. package/Components/MasterCell/utils/index.ts +18 -0
  11. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +40 -0
  12. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +55 -15
  13. package/Components/ModalComponent/BottomSheetModalContent.tsx +5 -1
  14. package/Components/ModalComponent/__tests__/showTextInput.test.ts +25 -0
  15. package/Components/ModalComponent/showTextInput.ts +2 -0
  16. package/Components/PlayerContainer/PlayerContainer.tsx +22 -5
  17. package/Components/PlayerContainer/__tests__/PlayerContainer.test.tsx +51 -0
  18. package/Components/River/ComponentsMap/ComponentsMap.tsx +4 -1
  19. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +2 -0
  20. package/Components/VideoLive/LiveImageManager.ts +126 -12
  21. package/Components/VideoLive/MuteUnmuteController.ts +135 -0
  22. package/Components/VideoLive/PlayerLiveImageComponent.tsx +15 -19
  23. package/Components/VideoLive/__tests__/LiveImageMuteState.test.ts +311 -0
  24. package/Components/VideoLive/__tests__/MuteUnmuteController.test.ts +135 -0
  25. package/Components/VideoLive/__tests__/PlayerLiveImageComponent.test.tsx +63 -1
  26. package/Components/VideoModal/PlayerWrapper.tsx +4 -1
  27. package/Components/VideoModal/__tests__/PlayerWrapper.test.tsx +1 -0
  28. package/Components/VideoModal/__tests__/playerWrapperStyle.test.ts +58 -0
  29. package/Components/VideoModal/__tests__/playerWrapperUtils.test.ts +16 -0
  30. package/Components/VideoModal/playerWrapperStyle.ts +21 -3
  31. package/Components/VideoModal/playerWrapperUtils.ts +7 -1
  32. package/Decorators/ZappPipesDataConnector/__tests__/screenToolbox.test.ts +347 -0
  33. package/Decorators/ZappPipesDataConnector/__tests__/screenToolboxUnavailable.test.ts +53 -0
  34. package/Decorators/ZappPipesDataConnector/index.tsx +19 -4
  35. package/Decorators/ZappPipesDataConnector/screenToolbox.ts +384 -0
  36. package/package.json +5 -5
@@ -1,10 +1,11 @@
1
1
  /* eslint-disable react/display-name */
2
2
  import * as React from "react";
3
- import { render } from "@testing-library/react-native";
3
+ import { act, render } from "@testing-library/react-native";
4
4
  import * as PlayerFactory from "@applicaster/zapp-react-native-utils/appUtils/playerManager/playerFactory";
5
5
  import { ThemeContext } from "@applicaster/zapp-react-native-utils/theme";
6
6
  import * as navigationHooks from "@applicaster/zapp-react-native-utils/reactHooks/navigation";
7
7
  import { playerManager } from "@applicaster/zapp-react-native-utils/appUtils/playerManager";
8
+ import { LiveImageManager, LiveImageType } from "../LiveImageManager";
8
9
 
9
10
  import { PlayerLiveImage } from "../PlayerLiveImageComponent";
10
11
  import * as useNavigationHooks from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useNavigation";
@@ -153,4 +154,65 @@ describe("PlayerLiveImageComponent", () => {
153
154
  const isPlayerRegistered = playerManager.isPlayerRegistered(props.playerId);
154
155
  expect(isPlayerRegistered).toBe(false);
155
156
  });
157
+
158
+ it("renders domain mute state after a pre-playback toggle and later player events", async () => {
159
+ jest.useFakeTimers();
160
+ const nativeView = jest.fn((_props: { muted: boolean }) => null);
161
+
162
+ const Player = jest.requireActual(
163
+ "@applicaster/zapp-react-native-utils/appUtils/playerManager/player"
164
+ ).Player;
165
+
166
+ const factory = jest
167
+ .spyOn(PlayerFactory, "playerFactory")
168
+ .mockImplementation(async (props) => ({
169
+ controller: new Player(props),
170
+ Component: nativeView,
171
+ }));
172
+
173
+ const register = jest.spyOn(LiveImageManager.instance, "register");
174
+
175
+ const view = render(
176
+ <Wrapper>
177
+ <PlayerLiveImage {...defaultProps} audioMutedByDefault />
178
+ </Wrapper>
179
+ );
180
+
181
+ const image = register.mock.calls[register.mock.calls.length - 1][0];
182
+
183
+ act(() => image.unmute());
184
+
185
+ view.rerender(
186
+ <Wrapper>
187
+ <PlayerLiveImage
188
+ {...defaultProps}
189
+ audioMutedByDefault
190
+ style={{ opacity: 1 }}
191
+ />
192
+ </Wrapper>
193
+ );
194
+
195
+ expect(image.getMuted()).toBe(false);
196
+
197
+ await act(async () => {
198
+ await image.prepareForPlayback();
199
+ image.setMode?.(LiveImageType.Video);
200
+ jest.advanceTimersByTime(1000);
201
+ });
202
+
203
+ expect(
204
+ nativeView.mock.calls[nativeView.mock.calls.length - 1][0].muted
205
+ ).toBe(false);
206
+
207
+ act(() => image.getPlayer()?.mute());
208
+
209
+ expect(
210
+ nativeView.mock.calls[nativeView.mock.calls.length - 1][0].muted
211
+ ).toBe(true);
212
+
213
+ view.unmount();
214
+ register.mockRestore();
215
+ factory.mockRestore();
216
+ jest.useRealTimers();
217
+ });
156
218
  });
@@ -14,6 +14,7 @@ type Props = {
14
14
  inline?: boolean;
15
15
  docked?: boolean;
16
16
  pip?: boolean;
17
+ vertical?: boolean;
17
18
  isModal?: boolean;
18
19
  fullscreen?: boolean;
19
20
  isTabletPortrait?: boolean;
@@ -32,6 +33,7 @@ const PlayerWrapperComponent = (props: Props) => {
32
33
  isTabletPortrait,
33
34
  configuration,
34
35
  pip,
36
+ vertical,
35
37
  playerContent,
36
38
  } = props;
37
39
 
@@ -42,6 +44,7 @@ const PlayerWrapperComponent = (props: Props) => {
42
44
  isTabletPortrait,
43
45
  configuration,
44
46
  pip,
47
+ vertical,
45
48
  });
46
49
 
47
50
  const isTablet = useIsTablet();
@@ -52,7 +55,7 @@ const PlayerWrapperComponent = (props: Props) => {
52
55
 
53
56
  return (
54
57
  <WrapperView
55
- edges={getEdges(isTablet, isInlineModal) as readonly Edge[]}
58
+ edges={getEdges(isTablet, isInlineModal, vertical) as readonly Edge[]}
56
59
  style={playerDimensionsHack}
57
60
  >
58
61
  <View
@@ -37,6 +37,7 @@ jest.mock(
37
37
 
38
38
  jest.mock("@applicaster/zapp-react-native-utils/reactHooks/device", () => ({
39
39
  useIsTablet: mockUseIsDeviceTablet,
40
+ useOrientation: () => "portrait",
40
41
  }));
41
42
 
42
43
  jest.mock("@applicaster/zapp-react-native-utils/theme", () => ({
@@ -0,0 +1,58 @@
1
+ import { renderHook } from "@testing-library/react-native";
2
+
3
+ import { useStyle } from "../playerWrapperStyle";
4
+ import { getScreenAspectRatio } from "../playerWrapperUtils";
5
+
6
+ let mockOrientation = "portrait";
7
+
8
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks", () => ({
9
+ useIsTablet: () => false,
10
+ useOrientation: () => mockOrientation,
11
+ }));
12
+
13
+ const getAspectRatio = (props: {
14
+ vertical: boolean;
15
+ inline?: boolean;
16
+ isModal?: boolean;
17
+ }) => {
18
+ const { result } = renderHook(() =>
19
+ useStyle({
20
+ style: {},
21
+ inline: false,
22
+ isModal: true,
23
+ isTabletPortrait: false,
24
+ configuration: {},
25
+ pip: false,
26
+ ...props,
27
+ })
28
+ );
29
+
30
+ return result.current.containerDimensions.aspectRatio;
31
+ };
32
+
33
+ describe("useStyle aspectRatio", () => {
34
+ const screenRatio = getScreenAspectRatio();
35
+
36
+ it("uses the screen ratio when not vertical", () => {
37
+ mockOrientation = "portrait";
38
+ expect(getAspectRatio({ vertical: false })).toBeCloseTo(screenRatio);
39
+ });
40
+
41
+ it("inverts the screen ratio for vertical fullscreen in portrait", () => {
42
+ mockOrientation = "portrait";
43
+ expect(getAspectRatio({ vertical: true })).toBeCloseTo(1 / screenRatio);
44
+ });
45
+
46
+ it("keeps the screen ratio for vertical fullscreen in landscape", () => {
47
+ mockOrientation = "landscape";
48
+ expect(getAspectRatio({ vertical: true })).toBeCloseTo(screenRatio);
49
+ });
50
+
51
+ it("inverts 16:9 for the vertical inline modal in landscape", () => {
52
+ mockOrientation = "landscape";
53
+
54
+ expect(getAspectRatio({ vertical: true, inline: true })).toBeCloseTo(
55
+ 9 / 16
56
+ );
57
+ });
58
+ });
@@ -0,0 +1,16 @@
1
+ import { getEdges } from "../playerWrapperUtils";
2
+
3
+ describe("getEdges", () => {
4
+ it("keeps the top edge in fullscreen when not vertical", () => {
5
+ expect(getEdges(false, false)).toEqual(["top"]);
6
+ });
7
+
8
+ it("has no edges in vertical fullscreen", () => {
9
+ expect(getEdges(false, false, true)).toEqual([]);
10
+ expect(getEdges(true, false, true)).toEqual([]);
11
+ });
12
+
13
+ it("keeps the top edge for the vertical inline modal", () => {
14
+ expect(getEdges(false, true, true)).toEqual(["top"]);
15
+ });
16
+ });
@@ -1,7 +1,10 @@
1
1
  import * as React from "react";
2
2
  import { StyleSheet, ViewStyle } from "react-native";
3
3
  import { isTV } from "@applicaster/zapp-react-native-utils/reactUtils";
4
- import { useIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks";
4
+ import {
5
+ useIsTablet,
6
+ useOrientation,
7
+ } from "@applicaster/zapp-react-native-utils/reactHooks";
5
8
  import { playerDimensionsHack } from "./utils";
6
9
  import { getTabletWidth } from "@applicaster/zapp-react-native-utils/playerUtils";
7
10
  import {
@@ -22,8 +25,10 @@ export const useStyle = ({
22
25
  isTabletPortrait,
23
26
  configuration,
24
27
  pip,
28
+ vertical,
25
29
  }) => {
26
30
  const isTablet = useIsTablet();
31
+ const isLandscape = useOrientation() === "landscape";
27
32
 
28
33
  const isInlineModal = inline && isModal;
29
34
 
@@ -40,10 +45,23 @@ export const useStyle = ({
40
45
  );
41
46
 
42
47
  const aspectRatio = React.useMemo(
43
- () => calculateAspectRatio(isInlineModal, pip),
48
+ () => {
49
+ const value = calculateAspectRatio(isInlineModal, pip);
50
+
51
+ if (!vertical) {
52
+ return value;
53
+ }
54
+
55
+ // vertical fullscreen in landscape: the width is the long side, keep the screen ratio so the container matches the screen
56
+ if (isLandscape && !isInlineModal && !pip) {
57
+ return value;
58
+ }
59
+
60
+ return 1 / value;
61
+ },
44
62
  // ignoring insets - only initial needed
45
63
  // eslint-disable-next-line react-hooks/exhaustive-deps
46
- [isInlineModal, pip]
64
+ [isInlineModal, pip, vertical, isLandscape]
47
65
  );
48
66
 
49
67
  return React.useMemo(() => {
@@ -44,8 +44,14 @@ export const getScreenAspectRatio = () => {
44
44
 
45
45
  export const getEdges = (
46
46
  isTablet: boolean,
47
- isInlineModal: boolean
47
+ isInlineModal: boolean,
48
+ vertical: boolean = false
48
49
  ): readonly Edge[] => {
50
+ // vertical fullscreen: the player takes the full screen height, a top inset would push it below the screen
51
+ if (vertical && !isInlineModal) {
52
+ return [];
53
+ }
54
+
49
55
  if (isTablet) {
50
56
  return ["top"];
51
57
  }
@@ -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
+ });