@applicaster/quick-brick-core 16.0.0-rc.99 → 17.0.0-alpha.2451166845

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 (52) hide show
  1. package/App/ActionSetters/index.ts +2 -2
  2. package/App/ActionsProvider/ActionsProvider.tsx +1 -1
  3. package/App/ActionsProvider/LegacyActionsRegistryAdapter.tsx +17 -1
  4. package/App/ActionsProvider/__tests__/LegacyActionsRegistryAdapter.test.tsx +64 -0
  5. package/App/AssetCache/logger.ts +1 -1
  6. package/App/DeepLinking/SamsungAppControl/__tests__/samsungAppControl.test.ts +255 -0
  7. package/App/DeepLinking/SamsungAppControl/index.ts +223 -0
  8. package/App/DeepLinking/URLSchemeHandler/SchemeHandlerHooks/usePresentSchemeHandler.ts +3 -0
  9. package/App/DeepLinking/URLSchemeListener/URLSchemeContextProvider.tsx +31 -1
  10. package/App/DeepLinking/URLSchemeListener/__tests__/URLSchemeListenerReplacement.test.tsx +103 -0
  11. package/App/DeepLinking/URLSchemeListener/index.tsx +14 -3
  12. package/App/DeepLinking/logger.ts +1 -1
  13. package/App/ErrorBoundary/index.tsx +5 -2
  14. package/App/NavigationProvider/NavigationProvider.tsx +17 -11
  15. package/App/NavigationProvider/__tests__/navToolbox.test.ts +227 -0
  16. package/App/NavigationProvider/__tests__/navigationProvider.test.tsx +56 -0
  17. package/App/NavigationProvider/navToolbox.ts +224 -0
  18. package/App/NavigationProvider/utils.ts +23 -2
  19. package/App/NetworkStatusProvider/NetworkStatusProvider.tsx +40 -4
  20. package/App/NetworkStatusProvider/ResumeVerificationController.ts +282 -0
  21. package/App/NetworkStatusProvider/__tests__/NetworkStatusProvider.test.tsx +167 -1
  22. package/App/NetworkStatusProvider/__tests__/ResumeVerificationController.test.ts +486 -0
  23. package/App/NetworkStatusProvider/__tests__/resumeVerification.test.ts +126 -0
  24. package/App/NetworkStatusProvider/__tests__/useResumeVerifiedNetInfo.test.ts +204 -0
  25. package/App/NetworkStatusProvider/resumeVerification.ts +54 -0
  26. package/App/NetworkStatusProvider/useResumeVerifiedNetInfo.ts +33 -0
  27. package/App/ThemeManager/index.tsx +6 -1
  28. package/App/appLifeCycleManager/index.tsx +2 -2
  29. package/App/appRemoteDataLoader/index.tsx +2 -2
  30. package/App/appRemoteDataLoader/remoteDataLoader.ts +2 -2
  31. package/App/index.tsx +8 -2
  32. package/App/layoutPreview/LayoutPreviewReminder.tsx +136 -0
  33. package/App/layoutPreview/__tests__/persistence.test.ts +63 -0
  34. package/App/layoutPreview/persistence.ts +228 -0
  35. package/App/logger.js +2 -2
  36. package/App/remoteContextReloader/__tests__/getNativeRemoteContextData.test.ts +148 -0
  37. package/App/remoteContextReloader/__tests__/getPromiseForType.test.js +31 -0
  38. package/App/remoteContextReloader/getRemoteContextData/getNativeRemoteContextData.ts +107 -29
  39. package/App/remoteContextReloader/helpers.ts +2 -1
  40. package/App/uiActions/index.ts +13 -0
  41. package/App/uiActions/muteUnmute/__tests__/assetResolver.test.ts +226 -0
  42. package/App/uiActions/muteUnmute/__tests__/binding.test.ts +51 -0
  43. package/App/uiActions/muteUnmute/__tests__/defaultAssets.test.ts +47 -0
  44. package/App/uiActions/muteUnmute/__tests__/entryState.test.ts +57 -0
  45. package/App/uiActions/muteUnmute/__tests__/labelResolver.test.ts +35 -0
  46. package/App/uiActions/muteUnmute/assetResolver.ts +87 -0
  47. package/App/uiActions/muteUnmute/defaultAssets.ts +44 -0
  48. package/App/uiActions/muteUnmute/entryState.ts +28 -0
  49. package/App/uiActions/muteUnmute/index.ts +79 -0
  50. package/App/uiActions/muteUnmute/labelResolver.ts +15 -0
  51. package/App/uiActions/muteUnmute/types.ts +18 -0
  52. package/package.json +8 -8
@@ -0,0 +1,103 @@
1
+ import * as React from "react";
2
+ import { Text } from "react-native";
3
+ import { act, render } from "@testing-library/react-native";
4
+
5
+ import { URLSchemeListener } from "../index";
6
+ import { URLSchemeContext } from "../URLSchemeContextProvider";
7
+
8
+ const mockMounts: string[] = [];
9
+ const mockFinishers: Record<string, (callback?: unknown) => void> = {};
10
+
11
+ jest.mock("../../URLSchemeHandler", () => {
12
+ const React = require("react");
13
+
14
+ return {
15
+ URLSchemeHandler: ({ url, onFinish }) => {
16
+ // Mirrors the real scheme handlers: work starts once, on mount.
17
+ React.useEffect(() => {
18
+ mockMounts.push(url);
19
+ mockFinishers[url] = onFinish;
20
+ // eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
21
+ }, []);
22
+
23
+ return null;
24
+ },
25
+ };
26
+ });
27
+
28
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/navigation", () => ({
29
+ useNavigation: () => ({ startUpHooks: null }),
30
+ }));
31
+
32
+ jest.mock("@applicaster/zapp-react-native-redux/hooks", () => ({
33
+ useAppSelector: () => true,
34
+ }));
35
+
36
+ const URL_A = "pureflix://open?feed_locator=a&id=1";
37
+ const URL_B = "pureflix://open?feed_locator=b&id=2";
38
+
39
+ const tree = (url: string | null, done: () => void) => (
40
+ <URLSchemeContext.Provider value={{ url, done }}>
41
+ <URLSchemeListener>
42
+ <Text>app</Text>
43
+ </URLSchemeListener>
44
+ </URLSchemeContext.Provider>
45
+ );
46
+
47
+ describe("URLSchemeListener - deep link replaced while being handled", () => {
48
+ beforeEach(() => {
49
+ mockMounts.length = 0;
50
+ Object.keys(mockFinishers).forEach((key) => delete mockFinishers[key]);
51
+ });
52
+
53
+ it("starts a new handler for a new URL", () => {
54
+ const done = jest.fn();
55
+ const { rerender } = render(tree(URL_A, done));
56
+
57
+ rerender(tree(URL_B, done));
58
+
59
+ expect(mockMounts).toEqual([URL_A, URL_B]);
60
+ });
61
+
62
+ it("ignores the finish of a replaced handler and lets the current one finish", () => {
63
+ const done = jest.fn();
64
+ const { rerender } = render(tree(URL_A, done));
65
+
66
+ rerender(tree(URL_B, done));
67
+
68
+ const staleNavigation = jest.fn((d) => d());
69
+
70
+ act(() => mockFinishers[URL_A](staleNavigation));
71
+ act(() => mockFinishers[URL_A]());
72
+
73
+ expect(staleNavigation).not.toHaveBeenCalled();
74
+ expect(done).not.toHaveBeenCalled();
75
+
76
+ const navigation = jest.fn((d) => d());
77
+
78
+ act(() => mockFinishers[URL_B](navigation));
79
+
80
+ expect(navigation).toHaveBeenCalledWith(done);
81
+ expect(done).toHaveBeenCalledTimes(1);
82
+ });
83
+
84
+ it("does not restart the handler when the same URL is set again", () => {
85
+ const done = jest.fn();
86
+ const { rerender } = render(tree(URL_A, done));
87
+
88
+ rerender(tree(URL_A, done));
89
+
90
+ expect(mockMounts).toEqual([URL_A]);
91
+
92
+ act(() => mockFinishers[URL_A]());
93
+
94
+ expect(done).toHaveBeenCalledTimes(1);
95
+ });
96
+
97
+ it("renders the app when there is no URL", () => {
98
+ const { getByText } = render(tree(null, jest.fn()));
99
+
100
+ expect(getByText("app")).toBeTruthy();
101
+ expect(mockMounts).toEqual([]);
102
+ });
103
+ });
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useContext } from "react";
1
+ import React, { useCallback, useContext, useRef } from "react";
2
2
 
3
3
  import { URLSchemeHandler } from "../URLSchemeHandler";
4
4
  import { URLSchemeContext } from "./URLSchemeContextProvider";
@@ -13,19 +13,30 @@ export function URLSchemeListener({ children }: { children: React.ReactNode }) {
13
13
 
14
14
  const appReady = useAppSelector(selectAppReady);
15
15
 
16
+ // The URL currently in the context. A handler compares its own URL with it on finish:
17
+ // a newer deep link may have replaced it while it was still resolving (e.g. loading a feed).
18
+ const latestUrl = useRef(url);
19
+ latestUrl.current = url;
20
+
16
21
  const onFinish = useCallback(
17
22
  (callback) => {
23
+ if (latestUrl.current !== url) {
24
+ // Stale handler: skip its navigation and do not clear the newer URL.
25
+ return;
26
+ }
27
+
18
28
  if (callback && typeof callback === "function") {
19
29
  callback?.(done);
20
30
  } else {
21
31
  done();
22
32
  }
23
33
  },
24
- [url]
34
+ [url, done]
25
35
  );
26
36
 
27
37
  if (url && !navigator.startUpHooks && appReady) {
28
- return <URLSchemeHandler url={url} onFinish={onFinish} />;
38
+ // key: a new URL remounts the handler so that its effects run for that URL.
39
+ return <URLSchemeHandler key={url} url={url} onFinish={onFinish} />;
29
40
  }
30
41
 
31
42
  return children;
@@ -5,6 +5,6 @@ import { coreAppLogger } from "../logger";
5
5
  export const { log_verbose, log_debug, log_warning, log_info, log_error } =
6
6
  createLogger({
7
7
  category: "DeepLinking",
8
- subsystem: "General",
8
+ subsystem: "app/deep_linking",
9
9
  parent: coreAppLogger,
10
10
  });
@@ -1,13 +1,16 @@
1
1
  import * as React from "react";
2
2
  import pick from "lodash/pick";
3
3
 
4
- import { coreLogger } from "@applicaster/zapp-react-native-utils/logger";
4
+ import { subsystemLogger } from "@applicaster/zapp-react-native-utils/logger";
5
5
  import { ErrorScreen } from "@applicaster/zapp-react-native-ui-components/Components/ErrorScreen";
6
6
 
7
7
  import { useErrorStore } from "./store";
8
8
  import * as QuickBrickManager from "@applicaster/zapp-react-native-bridge/QuickBrick";
9
9
 
10
- const errorBoundaryLogger = coreLogger.addSubsystem("react_error-boundary");
10
+ const errorBoundaryLogger = subsystemLogger(
11
+ "crash/error_boundary",
12
+ "ErrorBoundary"
13
+ );
11
14
 
12
15
  type Props = {
13
16
  children: React.ReactNode;
@@ -13,6 +13,8 @@ import {
13
13
  useGetScreenOrientation,
14
14
  } from "@applicaster/zapp-react-native-utils/appUtils/orientationHelper";
15
15
  import { focusManager } from "@applicaster/zapp-react-native-utils/focusManager/FocusManager";
16
+ import { setXrayNavigator } from "@applicaster/zapp-react-native-utils/navigationUtils/xrayNavigator";
17
+ import { registerNavToolbox } from "./navToolbox";
16
18
 
17
19
  import reducer, {
18
20
  ACTIONS,
@@ -43,7 +45,6 @@ import {
43
45
  startUpHookPluginIdentifiersSelector,
44
46
  } from "./navigator/selectors";
45
47
 
46
- import { coreAppLogger } from "../logger";
47
48
  import {
48
49
  getPlatform,
49
50
  isTV,
@@ -87,8 +88,9 @@ import {
87
88
  useLayoutVersion,
88
89
  useRivers,
89
90
  } from "@applicaster/zapp-react-native-utils/reactHooks";
91
+ import { subsystemLogger } from "@applicaster/zapp-react-native-utils/logger";
90
92
 
91
- const logger = coreAppLogger.addSubsystem("Navigator");
93
+ const logger = subsystemLogger("ui/navigation", "Navigator");
92
94
 
93
95
  type Props = {
94
96
  children?: React.ReactChild;
@@ -353,10 +355,7 @@ export function NavigationProvider({ children }: Props) {
353
355
  state: firstRoute.state,
354
356
  };
355
357
 
356
- if (
357
- stateForBack?.state?.entry &&
358
- "home" in stateForBack?.state?.entry
359
- ) {
358
+ if (stateForBack?.state?.entry?.home) {
360
359
  goHome();
361
360
  } else {
362
361
  dispatch(back(stateForBack));
@@ -763,11 +762,11 @@ export function NavigationProvider({ children }: Props) {
763
762
  const getStackForPathname = (
764
763
  pathname: string
765
764
  ): NavigationScreenState | undefined => {
766
- const stack = [...state?.stack?.mainStack];
767
-
768
- const currentStack = stack.find(({ route }) => route === pathname);
769
-
770
- return currentStack;
765
+ // Topmost match: the active screen if a route is on the stack twice.
766
+ return R.findLast(
767
+ ({ route }) => route === pathname,
768
+ state?.stack?.mainStack ?? []
769
+ );
771
770
  };
772
771
 
773
772
  const navigator: QuickBrickAppNavigator = React.useMemo(
@@ -832,6 +831,13 @@ export function NavigationProvider({ children }: Props) {
832
831
  navigator.state = state;
833
832
  }
834
833
 
834
+ React.useLayoutEffect(() => {
835
+ setXrayNavigator(navigator);
836
+ registerNavToolbox();
837
+
838
+ return () => setXrayNavigator(undefined);
839
+ }, [navigator]);
840
+
835
841
  React.useLayoutEffect(() => {
836
842
  if (pathname === "/" && appLaunched) {
837
843
  goHome(true);
@@ -0,0 +1,227 @@
1
+ const mockRegister = jest.fn().mockResolvedValue(undefined);
2
+
3
+ // The X-Ray mock (__mocks__/@applicaster/quick-brick-xray) has no toolbox API: give it one.
4
+ require("@applicaster/quick-brick-xray").registerReactToolbox = mockRegister;
5
+
6
+ // Required after the toolbox API is in place: loading them can register the player toolbox.
7
+ const {
8
+ modalStore,
9
+ } = require("@applicaster/zapp-react-native-utils/modalState");
10
+
11
+ const {
12
+ setXrayNavigator,
13
+ } = require("@applicaster/zapp-react-native-utils/navigationUtils/xrayNavigator");
14
+
15
+ const {
16
+ playerManager,
17
+ } = require("@applicaster/zapp-react-native-utils/appUtils/playerManager");
18
+
19
+ const { registerNavToolbox } = require("../navToolbox");
20
+
21
+ const navigator = (overrides = {}) => {
22
+ const nav = {
23
+ getPathname: () => "/river/a/river/b",
24
+ getStack: () => [{ route: "/river/a" }, { route: "/river/a/river/b" }],
25
+ canGoBack: () => true,
26
+ goBack: jest.fn(),
27
+ goHome: jest.fn(),
28
+ getPlayNextOverlay: () => null,
29
+ getModalState: () => modalStore.getState().modalState,
30
+ dismissModal: jest.fn(),
31
+ videoModalState: { visible: false },
32
+ minimiseVideoModal: jest.fn(),
33
+ maximiseVideoModal: jest.fn(),
34
+ closeVideoModal: jest.fn(),
35
+ ...overrides,
36
+ };
37
+
38
+ setXrayNavigator(nav as unknown as QuickBrickAppNavigator);
39
+
40
+ return nav;
41
+ };
42
+
43
+ const videoModal = (mode: string) => ({
44
+ videoModalState: { visible: true, mode, item: { id: "v1", title: "V" } },
45
+ });
46
+
47
+ const call = (name: string) => {
48
+ // Other toolboxes (player) may register first.
49
+ const { tools } = mockRegister.mock.calls.find(
50
+ ([toolbox]) => toolbox.name === "nav"
51
+ )[0];
52
+
53
+ return tools.find((tool) => tool.name === name).handler({});
54
+ };
55
+
56
+ describe("navToolbox", () => {
57
+ beforeAll(() => registerNavToolbox());
58
+
59
+ afterEach(() => {
60
+ modalStore.getState().setModalState({ visible: false, screen: null });
61
+ jest.restoreAllMocks();
62
+ });
63
+
64
+ it("throws until the NavigationProvider sets the navigator", () => {
65
+ setXrayNavigator(undefined);
66
+
67
+ expect(() => call("stack")).toThrow("Navigation is not available yet");
68
+ });
69
+
70
+ it("goes back one screen", () => {
71
+ const nav = navigator();
72
+
73
+ expect(call("back")).toEqual({ from: "/river/a/river/b", wentBack: true });
74
+ expect(nav.goBack).toHaveBeenCalledWith(false);
75
+ });
76
+
77
+ it("does not go back from the root", () => {
78
+ const nav = navigator({ canGoBack: () => false });
79
+
80
+ expect(call("back")).toMatchObject({ wentBack: false });
81
+ expect(nav.goBack).not.toHaveBeenCalled();
82
+ });
83
+
84
+ it("does not pop the screen under a modal or the video modal", () => {
85
+ const nav = navigator(videoModal("MAXIMIZED"));
86
+
87
+ expect(call("back")).toEqual({
88
+ from: "video-modal/v1",
89
+ wentBack: false,
90
+ blockedBy: "video-modal",
91
+ });
92
+
93
+ modalStore.getState().openModal({ item: { id: "m" } });
94
+
95
+ expect(call("back")).toMatchObject({ blockedBy: "modal" });
96
+ expect(nav.goBack).not.toHaveBeenCalled();
97
+ });
98
+
99
+ it("only dismisses Play Next when it shows", () => {
100
+ const nav = navigator({ getPlayNextOverlay: () => ({}) });
101
+
102
+ expect(call("back")).toMatchObject({
103
+ wentBack: false,
104
+ dismissed: "playNext",
105
+ });
106
+
107
+ expect(nav.goBack).toHaveBeenCalledWith(false);
108
+ });
109
+
110
+ it("leaves a hook screen to cancel itself", () => {
111
+ const nav = navigator({ getPathname: () => "/river/a/hooks/login" });
112
+
113
+ expect(call("back")).toMatchObject({ wentBack: false, blockedBy: "hook" });
114
+ expect(nav.goBack).not.toHaveBeenCalled();
115
+ });
116
+
117
+ it("goes home", () => {
118
+ const nav = navigator();
119
+
120
+ expect(call("home")).toEqual({ from: "/river/a/river/b", wentHome: true });
121
+ expect(nav.goHome).toHaveBeenCalled();
122
+ });
123
+
124
+ it("does not go home under a modal or the video modal", () => {
125
+ const nav = navigator(videoModal("MAXIMIZED"));
126
+
127
+ expect(call("home")).toEqual({
128
+ from: "video-modal/v1",
129
+ wentHome: false,
130
+ blockedBy: "video-modal",
131
+ });
132
+
133
+ modalStore.getState().openModal({ item: { id: "m" } });
134
+
135
+ expect(call("home")).toMatchObject({ blockedBy: "modal" });
136
+ expect(nav.goHome).not.toHaveBeenCalled();
137
+ });
138
+
139
+ it("goes home with the mini player docked", () => {
140
+ const nav = navigator(videoModal("MINIMIZED"));
141
+
142
+ expect(call("home")).toMatchObject({ wentHome: true });
143
+ expect(nav.goHome).toHaveBeenCalled();
144
+ });
145
+
146
+ it("dismisses the modal store's modal, not the video modal", () => {
147
+ const nav = navigator(videoModal("MAXIMIZED"));
148
+
149
+ expect(() => call("dismissModal")).toThrow("No modal is open");
150
+
151
+ modalStore.getState().openModal({ item: { id: "m" } });
152
+
153
+ expect(call("dismissModal")).toBe("OK");
154
+ expect(nav.dismissModal).toHaveBeenCalled();
155
+ });
156
+
157
+ it("minimises only a maximised video modal", () => {
158
+ const fullscreen = navigator(videoModal("FULLSCREEN"));
159
+
160
+ expect(() => call("minimizeVideoModal")).toThrow("mode is FULLSCREEN");
161
+ expect(fullscreen.minimiseVideoModal).not.toHaveBeenCalled();
162
+
163
+ const maximized = navigator(videoModal("MAXIMIZED"));
164
+
165
+ call("minimizeVideoModal");
166
+
167
+ expect(maximized.minimiseVideoModal).toHaveBeenCalled();
168
+ });
169
+
170
+ it("enters and leaves fullscreen through the player, which also rotates the phone", () => {
171
+ const invoke = jest.spyOn(playerManager, "invokeHandler");
172
+ const playerContainer = jest.fn();
173
+
174
+ playerManager.on("toggleFullscreen", playerContainer);
175
+
176
+ navigator(videoModal("MAXIMIZED"));
177
+ call("fullscreenVideoModal");
178
+
179
+ expect(invoke).toHaveBeenCalledWith("toggleFullscreen", {
180
+ fullscreen: true,
181
+ });
182
+
183
+ const fullscreen = navigator(videoModal("FULLSCREEN"));
184
+
185
+ call("maximizeVideoModal");
186
+
187
+ expect(invoke).toHaveBeenCalledWith("toggleFullscreen", {
188
+ fullscreen: false,
189
+ });
190
+
191
+ expect(fullscreen.maximiseVideoModal).not.toHaveBeenCalled();
192
+
193
+ playerManager.removeHandler("toggleFullscreen", playerContainer);
194
+ });
195
+
196
+ it("does not claim fullscreen without a player, or from the mini player", () => {
197
+ navigator(videoModal("MAXIMIZED"));
198
+
199
+ expect(() => call("fullscreenVideoModal")).toThrow("shows no player");
200
+
201
+ navigator(videoModal("MINIMIZED"));
202
+
203
+ expect(() => call("fullscreenVideoModal")).toThrow("mode is MINIMIZED");
204
+ });
205
+
206
+ it("expands the mini player", () => {
207
+ const nav = navigator(videoModal("MINIMIZED"));
208
+
209
+ call("maximizeVideoModal");
210
+
211
+ expect(nav.maximiseVideoModal).toHaveBeenCalled();
212
+ });
213
+
214
+ it("reports the video modal", () => {
215
+ navigator(videoModal("MINIMIZED"));
216
+
217
+ expect(call("videoModal")).toMatchObject({
218
+ visible: true,
219
+ mode: "MINIMIZED",
220
+ entry: { id: "v1", title: "V" },
221
+ });
222
+
223
+ navigator();
224
+
225
+ expect(call("videoModal")).toMatchObject({ visible: false });
226
+ });
227
+ });
@@ -410,6 +410,62 @@ describe("<NavigationProvider />", () => {
410
410
  });
411
411
  });
412
412
 
413
+ describe("when backToTop is requested from a non-home tab", () => {
414
+ const listenRiver = {
415
+ id: "LISTEN",
416
+ home: false,
417
+ navigations: [],
418
+ ui_components: [],
419
+ };
420
+
421
+ beforeEach(() => {
422
+ wrapper = renderContextConsumer({
423
+ ...store,
424
+ rivers: { ...rivers, LISTEN: listenRiver },
425
+ });
426
+
427
+ act(() => {
428
+ getNavigatorValues(wrapper).navigator.replace(listenRiver);
429
+ });
430
+ });
431
+
432
+ it("returns to the tab root from the second level", () => {
433
+ act(() => {
434
+ getNavigatorValues(wrapper).navigator.push(rivers.A1234);
435
+ });
436
+
437
+ act(() => {
438
+ getNavigatorValues(wrapper).navigator.goBack(false, false, true);
439
+ });
440
+
441
+ const { navigator, state } = getNavigatorValues(wrapper);
442
+
443
+ expect(navigator.currentRoute).toEqual("/river/LISTEN");
444
+ expect(navigator.activeRiver).toEqual(listenRiver);
445
+ expect(state.stack.mainStack).toHaveLength(1);
446
+ });
447
+
448
+ it("returns to the tab root from the third level", () => {
449
+ act(() => {
450
+ getNavigatorValues(wrapper).navigator.push(rivers.A1234);
451
+ });
452
+
453
+ act(() => {
454
+ getNavigatorValues(wrapper).navigator.push(rivers.C0987);
455
+ });
456
+
457
+ act(() => {
458
+ getNavigatorValues(wrapper).navigator.goBack(false, false, true);
459
+ });
460
+
461
+ const { navigator, state } = getNavigatorValues(wrapper);
462
+
463
+ expect(navigator.currentRoute).toEqual("/river/LISTEN");
464
+ expect(navigator.activeRiver).toEqual(listenRiver);
465
+ expect(state.stack.mainStack).toHaveLength(1);
466
+ });
467
+ });
468
+
413
469
  describe("when there is a previous route and there are hooks", () => {
414
470
  beforeEach(() => {
415
471
  wrapper = renderContextConsumer();