@applicaster/quick-brick-core 13.0.0-alpha.3962361544 → 13.0.0-alpha.4012243505

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.
@@ -0,0 +1,232 @@
1
+ import React from "react";
2
+ import { View, Text, TouchableOpacity } from "react-native";
3
+ import { render, fireEvent, act } from "@testing-library/react-native";
4
+ import { useErrorStore } from "../store";
5
+ import { ErrorBoundary } from "../index";
6
+
7
+ jest.mock(
8
+ "@applicaster/zapp-react-native-ui-components/Components/ErrorScreen",
9
+ () => ({
10
+ // eslint-disable-next-line react/prop-types
11
+ ErrorScreen: ({ dismissError, message, buttons }) => (
12
+ <View testID="error-screen">
13
+ <Text testID="error-message">{message || "Default error message"}</Text>
14
+ {/* eslint-disable-next-line react/prop-types */}
15
+ {buttons?.map((button, index) => (
16
+ <TouchableOpacity
17
+ key={index}
18
+ onPress={button.onPress}
19
+ testID={`button-${index}`}
20
+ >
21
+ <Text>{button.text}</Text>
22
+ </TouchableOpacity>
23
+ ))}
24
+
25
+ {dismissError ? (
26
+ <TouchableOpacity onPress={dismissError} testID="dismiss-button">
27
+ <Text>Dismiss Error</Text>
28
+ </TouchableOpacity>
29
+ ) : null}
30
+ </View>
31
+ ),
32
+ })
33
+ );
34
+
35
+ jest.mock("@applicaster/zapp-react-native-bridge/QuickBrick", () => ({
36
+ sendQuickBrickEvent: jest.fn(),
37
+ QUICK_BRICK_EVENTS: {
38
+ FORCE_APP_RELOAD: "force_app_reload",
39
+ },
40
+ }));
41
+
42
+ jest.mock("@applicaster/zapp-react-native-utils/logger", () => ({
43
+ coreLogger: {
44
+ addSubsystem: () => ({
45
+ error: jest.fn(),
46
+ }),
47
+ },
48
+ }));
49
+
50
+ // eslint-disable-next-line react/prop-types
51
+ const TestComponent = ({ error, errorMessage = "error !!" }) => {
52
+ if (error) {
53
+ throw new Error(errorMessage);
54
+ }
55
+
56
+ return (
57
+ <View>
58
+ <Text>all is good</Text>
59
+ </View>
60
+ );
61
+ };
62
+
63
+ describe("<ErrorBoundary />", () => {
64
+ // Clear all error states between tests
65
+ beforeEach(() => {
66
+ useErrorStore.getState().dismissError();
67
+ });
68
+
69
+ describe("when there is no error", () => {
70
+ it("renders children correctly", () => {
71
+ const { getByText } = render(
72
+ <ErrorBoundary>
73
+ <TestComponent />
74
+ </ErrorBoundary>
75
+ );
76
+
77
+ expect(getByText("all is good")).toBeTruthy();
78
+ });
79
+ });
80
+
81
+ describe("when an error is caught", () => {
82
+ const _consoleLogs = {
83
+ error: console.error,
84
+ log: console.log,
85
+ };
86
+
87
+ beforeAll(() => {
88
+ console.error = jest.fn();
89
+ console.log = jest.fn();
90
+ });
91
+
92
+ afterAll(() => {
93
+ console.error = _consoleLogs.error;
94
+ console.log = _consoleLogs.log;
95
+ });
96
+
97
+ it("renders error state with default values (recoverable error)", () => {
98
+ const { getByTestId } = render(
99
+ <ErrorBoundary>
100
+ <TestComponent error />
101
+ </ErrorBoundary>
102
+ );
103
+
104
+ expect(getByTestId("error-screen")).toBeTruthy();
105
+ expect(getByTestId("dismiss-button")).toBeTruthy(); // Should be recoverable by default
106
+ });
107
+
108
+ it("handles non-recoverable errors (no dismiss button)", () => {
109
+ const { getByTestId, queryByTestId } = render(
110
+ <ErrorBoundary recoverable={false}>
111
+ <TestComponent error />
112
+ </ErrorBoundary>
113
+ );
114
+
115
+ expect(getByTestId("error-screen")).toBeTruthy();
116
+ expect(queryByTestId("dismiss-button")).toBeFalsy(); // Should not be recoverable
117
+ });
118
+
119
+ it("shows custom error message from caught error", () => {
120
+ const customErrorMessage = "Custom error message for testing";
121
+
122
+ const { getByTestId } = render(
123
+ <ErrorBoundary>
124
+ <TestComponent error errorMessage={customErrorMessage} />
125
+ </ErrorBoundary>
126
+ );
127
+
128
+ // The error info should be passed to the ErrorScreen
129
+ expect(getByTestId("error-message").props.children).toBeTruthy();
130
+ });
131
+
132
+ it("can dismiss the error and restore the UI", () => {
133
+ // First render with error
134
+ const { getByTestId, queryByText, rerender } = render(
135
+ <ErrorBoundary>
136
+ <TestComponent error />
137
+ </ErrorBoundary>
138
+ );
139
+
140
+ // Verify error screen is shown
141
+ expect(getByTestId("error-screen")).toBeTruthy();
142
+
143
+ // Dismiss the error
144
+ fireEvent.press(getByTestId("dismiss-button"));
145
+
146
+ // Force rerender to see the change
147
+ rerender(
148
+ <ErrorBoundary>
149
+ <TestComponent />
150
+ </ErrorBoundary>
151
+ );
152
+
153
+ // Now the regular content should be visible
154
+ expect(queryByText("all is good")).toBeTruthy();
155
+ });
156
+ });
157
+
158
+ describe("error store functionality", () => {
159
+ it("properly sets error state", () => {
160
+ const error = new Error("Test error");
161
+ const errorInfo = "Test error info";
162
+
163
+ act(() => {
164
+ useErrorStore.getState().setError(error, errorInfo, true);
165
+ });
166
+
167
+ const state = useErrorStore.getState();
168
+ expect(state.hasError).toBe(true);
169
+ expect(state.error).toBe(error);
170
+ expect(state.info).toBe(errorInfo);
171
+ expect(state.recoverable).toBe(true);
172
+ });
173
+
174
+ it("properly resets error state", () => {
175
+ // First set an error
176
+ act(() => {
177
+ useErrorStore
178
+ .getState()
179
+ .setError(new Error("Test error"), "Info", true);
180
+ });
181
+
182
+ // Then dismiss it
183
+ act(() => {
184
+ useErrorStore.getState().dismissError();
185
+ });
186
+
187
+ const state = useErrorStore.getState();
188
+ expect(state.hasError).toBe(false);
189
+ expect(state.error).toBeUndefined();
190
+ expect(state.info).toBeUndefined();
191
+ expect(state.recoverable).toBe(true); // Default value when reset
192
+ });
193
+
194
+ it("remembers recoverable state", () => {
195
+ act(() => {
196
+ useErrorStore
197
+ .getState()
198
+ .setError(new Error("Test error"), "Info", false);
199
+ });
200
+
201
+ const state = useErrorStore.getState();
202
+ expect(state.recoverable).toBe(false);
203
+ });
204
+ });
205
+
206
+ describe("integration with ErrorScreen", () => {
207
+ it("renders restart app button for non-recoverable errors", () => {
208
+ const { getAllByTestId } = render(
209
+ <ErrorBoundary recoverable={false}>
210
+ <TestComponent error />
211
+ </ErrorBoundary>
212
+ );
213
+
214
+ // Should have the restart app button
215
+ const buttons = getAllByTestId(/button-/);
216
+ expect(buttons.length).toBeGreaterThan(0);
217
+ });
218
+
219
+ it("passes error info to the ErrorScreen component", () => {
220
+ const customErrorMessage = "Very specific error message";
221
+
222
+ const { getByTestId } = render(
223
+ <ErrorBoundary>
224
+ <TestComponent error errorMessage={customErrorMessage} />
225
+ </ErrorBoundary>
226
+ );
227
+
228
+ // The error info should be visible in the UI
229
+ expect(getByTestId("error-message")).toBeTruthy();
230
+ });
231
+ });
232
+ });
@@ -0,0 +1,143 @@
1
+ import { act, renderHook } from "@testing-library/react-hooks";
2
+ import { useErrorStore } from "../store";
3
+
4
+ describe("Error Store", () => {
5
+ beforeEach(() => {
6
+ act(() => {
7
+ useErrorStore.getState().dismissError();
8
+ });
9
+ });
10
+
11
+ describe("Initial state", () => {
12
+ it("should have correct default values", () => {
13
+ const { result } = renderHook(() => useErrorStore());
14
+
15
+ expect(result.current).toMatchObject({
16
+ hasError: false,
17
+ error: undefined,
18
+ info: undefined,
19
+ recoverable: true,
20
+ setError: expect.any(Function),
21
+ dismissError: expect.any(Function),
22
+ });
23
+ });
24
+ });
25
+
26
+ describe("setError action", () => {
27
+ it("should set error state with provided values", () => {
28
+ const testError = new Error("Test error");
29
+ const testInfo = "Test error info";
30
+ const isRecoverable = true;
31
+
32
+ const { result } = renderHook(() => useErrorStore());
33
+
34
+ act(() => {
35
+ result.current.setError(testError, testInfo, isRecoverable);
36
+ });
37
+
38
+ expect(result.current).toMatchObject({
39
+ hasError: true,
40
+ error: testError,
41
+ info: testInfo,
42
+ recoverable: isRecoverable,
43
+ setError: expect.any(Function),
44
+ dismissError: expect.any(Function),
45
+ });
46
+ });
47
+
48
+ it("should set non-recoverable error when specified", () => {
49
+ const testError = new Error("Test error");
50
+ const testInfo = "Test error info";
51
+ const isRecoverable = false;
52
+
53
+ const { result } = renderHook(() => useErrorStore());
54
+
55
+ act(() => {
56
+ result.current.setError(testError, testInfo, isRecoverable);
57
+ });
58
+
59
+ expect(result.current).toMatchObject({
60
+ recoverable: false,
61
+ });
62
+ });
63
+
64
+ it("should default to recoverable error when not specified", () => {
65
+ const testError = new Error("Test error");
66
+ const testInfo = "Test error info";
67
+
68
+ const { result } = renderHook(() => useErrorStore());
69
+
70
+ act(() => {
71
+ result.current.setError(testError, testInfo);
72
+ });
73
+
74
+ expect(result.current).toMatchObject({
75
+ recoverable: true,
76
+ });
77
+ });
78
+ });
79
+
80
+ describe("dismissError action", () => {
81
+ it("should reset error state to defaults", () => {
82
+ const testError = new Error("Test error");
83
+ const { result } = renderHook(() => useErrorStore());
84
+
85
+ act(() => {
86
+ result.current.setError(testError, "info", false);
87
+ });
88
+
89
+ expect(result.current).toMatchObject({
90
+ hasError: true,
91
+ recoverable: false,
92
+ });
93
+
94
+ act(() => {
95
+ result.current.dismissError();
96
+ });
97
+
98
+ expect(result.current).toMatchObject({
99
+ hasError: false,
100
+ error: undefined,
101
+ info: undefined,
102
+ recoverable: true,
103
+ setError: expect.any(Function),
104
+ dismissError: expect.any(Function),
105
+ });
106
+ });
107
+ });
108
+
109
+ describe("Direct store access via API", () => {
110
+ it("should allow direct access to store outside React components", () => {
111
+ act(() => {
112
+ useErrorStore
113
+ .getState()
114
+ .setError(new Error("API error"), "API info", false);
115
+ });
116
+
117
+ const { result } = renderHook(() => useErrorStore());
118
+
119
+ expect(result.current).toMatchObject({
120
+ hasError: true,
121
+ error: expect.objectContaining({ message: "API error" }),
122
+ info: "API info",
123
+ recoverable: false,
124
+ });
125
+ });
126
+
127
+ it("should allow clearing errors via API", () => {
128
+ act(() => {
129
+ useErrorStore.getState().setError(new Error("API error"), "API info");
130
+ });
131
+
132
+ act(() => {
133
+ useErrorStore.getState().dismissError();
134
+ });
135
+
136
+ const { result } = renderHook(() => useErrorStore());
137
+
138
+ expect(result.current).toMatchObject({
139
+ hasError: false,
140
+ });
141
+ });
142
+ });
143
+ });
@@ -1,42 +1,66 @@
1
1
  import * as React from "react";
2
+ import pick from "lodash/pick";
2
3
 
3
4
  import { coreLogger } from "@applicaster/zapp-react-native-utils/logger";
4
5
  import { ErrorScreen } from "@applicaster/zapp-react-native-ui-components/Components/ErrorScreen";
5
6
 
7
+ import { useErrorStore } from "./store";
8
+ import * as QuickBrickManager from "@applicaster/zapp-react-native-bridge/QuickBrick";
9
+
6
10
  const errorBoundaryLogger = coreLogger.addSubsystem("react_error-boundary");
7
11
 
8
12
  type Props = {
9
13
  children: React.ReactNode;
14
+ recoverable?: boolean;
10
15
  };
11
16
 
12
- type State = {
13
- hasError: boolean;
14
- error?: Error;
15
- info?: string;
17
+ type ErrorBoundaryContentProps = {
18
+ children: React.ReactNode;
16
19
  };
17
20
 
18
- export class ErrorBoundary extends React.Component<Props, State> {
19
- state: State = {
20
- hasError: false,
21
- error: undefined,
22
- };
21
+ const restartApp = () => {
22
+ QuickBrickManager.sendQuickBrickEvent(
23
+ QuickBrickManager.QUICK_BRICK_EVENTS.FORCE_APP_RELOAD
24
+ );
25
+ };
23
26
 
24
- componentDidCatch(error, info) {
25
- errorBoundaryLogger.error({ error, data: info, message: error.message });
26
- this.setState({ hasError: true, error, info });
27
+ const restartButton = {
28
+ text: "Restart the app",
29
+ onPress: restartApp,
30
+ };
31
+
32
+ const buttons = [restartButton];
33
+
34
+ const ErrorBoundaryContent: React.FC<ErrorBoundaryContentProps> = ({
35
+ children,
36
+ }) => {
37
+ const { hasError, dismissError, info } = pick(
38
+ useErrorStore((state) => state),
39
+ ["hasError", "dismissError", "info"]
40
+ );
41
+
42
+ if (hasError) {
43
+ return (
44
+ <ErrorScreen
45
+ dismissError={dismissError}
46
+ message={info}
47
+ buttons={buttons}
48
+ />
49
+ );
27
50
  }
28
51
 
29
- dismissError = () => {
30
- this.setState({ hasError: false });
31
- };
52
+ return <>{children}</>;
53
+ };
32
54
 
33
- render() {
34
- const { hasError } = this.state as State;
55
+ export class ErrorBoundary extends React.Component<Props> {
56
+ componentDidCatch(error, info) {
57
+ errorBoundaryLogger.error({ error, data: info, message: error.message });
58
+ const { recoverable = true } = this.props;
35
59
 
36
- if (hasError) {
37
- return <ErrorScreen dismissError={this.dismissError} />;
38
- }
60
+ useErrorStore.getState().setError(error, info, recoverable);
61
+ }
39
62
 
40
- return this.props.children;
63
+ render() {
64
+ return <ErrorBoundaryContent>{this.props.children}</ErrorBoundaryContent>;
41
65
  }
42
66
  }
@@ -0,0 +1,26 @@
1
+ import { create } from "zustand";
2
+
3
+ type ErrorState = {
4
+ hasError: boolean;
5
+ error?: Error;
6
+ info?: string;
7
+ recoverable: boolean;
8
+ setError: (error: Error, info: string, recoverable?: boolean) => void;
9
+ dismissError: () => void;
10
+ };
11
+
12
+ export const useErrorStore = create<ErrorState>((set) => ({
13
+ hasError: false,
14
+ error: undefined,
15
+ info: undefined,
16
+ recoverable: true,
17
+ setError: (error: Error, info: string, recoverable = true) =>
18
+ set({ hasError: true, error, info, recoverable }),
19
+ dismissError: () =>
20
+ set({
21
+ hasError: false,
22
+ error: undefined,
23
+ info: undefined,
24
+ recoverable: true,
25
+ }),
26
+ }));
@@ -2,7 +2,7 @@ import React from "react";
2
2
  import { StyleSheet, TouchableOpacity, Easing, View } from "react-native";
3
3
 
4
4
  import { AnimatedInOut } from "@applicaster/zapp-react-native-ui-components/Components/AnimatedInOut";
5
- import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks/navigation";
5
+ import { dismissModal } from "@applicaster/zapp-react-native-utils/modalState";
6
6
 
7
7
  type Props = {
8
8
  overlayColor: string;
@@ -35,21 +35,19 @@ const fadeInAnimation = {
35
35
  delay,
36
36
  };
37
37
 
38
- const animationConfig = (onAnimationEnd) => ({
38
+ const animationConfig = {
39
39
  in: fadeInAnimation,
40
40
  out: {
41
41
  ...fadeInAnimation,
42
- onAnimationEnd,
42
+ onAnimationEnd: dismissModal,
43
43
  },
44
- });
44
+ };
45
45
 
46
46
  export function Overlay(props: Props) {
47
- const navigator = useNavigation();
48
-
49
47
  return (
50
48
  <AnimatedInOut
51
49
  visible={props.visible}
52
- animationConfig={animationConfig(navigator.closeModalBottomSheet)}
50
+ animationConfig={animationConfig}
53
51
  staticStyles={styles.container}
54
52
  >
55
53
  <TouchableOpacity
@@ -3,7 +3,7 @@ import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
3
3
  import { platformSelect } from "@applicaster/zapp-react-native-utils/reactUtils";
4
4
  import { useSafeAreaInsets } from "react-native-safe-area-context";
5
5
 
6
- import React, { useCallback, useEffect, useMemo, useState } from "react";
6
+ import React, { useCallback, useEffect, useState } from "react";
7
7
  import { Keyboard, StyleSheet, View } from "react-native";
8
8
 
9
9
  import { Overlay } from "./Overlay";
@@ -75,13 +75,7 @@ export function ModalBottomSheet(props: Props) {
75
75
 
76
76
  const isTablet = useIsTablet();
77
77
 
78
- const maxWidth = useMemo(
79
- () =>
80
- isTablet
81
- ? Math.max(sheetTabletMaxWidth, dimensions.width)
82
- : dimensions.width,
83
- [isTablet]
84
- );
78
+ const maxWidth = isTablet ? sheetTabletMaxWidth : dimensions.width;
85
79
 
86
80
  const modalVisible = visible && sheetHeight > 0;
87
81
 
@@ -6,10 +6,10 @@ import { ModalChildrenWrapper } from "./ModalChildrenWrapper";
6
6
  import { PathnameContext } from "@applicaster/zapp-react-native-ui-components/Contexts/PathnameContext";
7
7
  import { ScreenContextProvider } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenContext";
8
8
  import { ROUTE_TYPES } from "@applicaster/zapp-react-native-utils/navigationUtils/routeTypes";
9
- import { useModalStore } from "@applicaster/zapp-react-native-utils/modalState";
9
+ import { useModalStoreState } from "@applicaster/zapp-react-native-utils/modalState";
10
10
 
11
11
  export function ModalProvider() {
12
- const { modalState } = useModalStore();
12
+ const modalState = useModalStoreState();
13
13
 
14
14
  const [modalVisible, setModalVisible] = React.useState(false);
15
15
  const [modalShown, setModalShown] = React.useState(false);
@@ -58,13 +58,20 @@ import {
58
58
  legacyScreenData,
59
59
  openExternalUrl,
60
60
  } from "./utils";
61
- import { debounce } from "@applicaster/zapp-react-native-utils/functionUtils";
61
+ import {
62
+ debounce,
63
+ getJsonReplacer,
64
+ } from "@applicaster/zapp-react-native-utils/functionUtils";
62
65
 
63
66
  import { findParent } from "@applicaster/zapp-react-native-ui-components/Contexts/ZappPipesContext/ZappPipesEntryContext";
64
67
  import { ZappPipesEntryContext } from "@applicaster/zapp-react-native-ui-components/Contexts";
65
68
  import { PlayNextState } from "@applicaster/zapp-react-native-utils/appUtils/playerManager/OverlayObserver/OverlaysObserver";
66
69
  import { inflateUrl } from "@applicaster/zapp-react-native-ui-components/Components/LinkHandler/LinkHandler";
67
- import { useModalStore } from "@applicaster/zapp-react-native-utils/modalState";
70
+ import {
71
+ dismissModal,
72
+ modalStore,
73
+ openModal,
74
+ } from "@applicaster/zapp-react-native-utils/modalState";
68
75
  import { useSubscriberFor } from "@applicaster/zapp-react-native-utils/reactHooks/useSubscriberFor";
69
76
  import { QBUIComponentEvents } from "@applicaster/zapp-react-native-ui-components/events";
70
77
  import { Hook } from "@applicaster/zapp-react-native-utils/appUtils/HooksManager/Hook";
@@ -120,8 +127,6 @@ export function NavigationProvider({ children }: Props) {
120
127
 
121
128
  const { context } = React.useContext(ZappPipesEntryContext.Context);
122
129
 
123
- const { openModal, dismissModal } = useModalStore();
124
-
125
130
  const playNextOverlayState = React.useRef();
126
131
 
127
132
  const online = useConnectionInfo(true);
@@ -147,7 +152,7 @@ export function NavigationProvider({ children }: Props) {
147
152
  );
148
153
 
149
154
  const getModalState: () => ModalState = () => {
150
- const { modalState } = useModalStore.getState();
155
+ const { modalState } = modalStore.getState();
151
156
 
152
157
  return modalState;
153
158
  };
@@ -391,7 +396,7 @@ export function NavigationProvider({ children }: Props) {
391
396
  }
392
397
 
393
398
  if (hookPlugin.isModalHook()) {
394
- navigator.openModal({
399
+ openModal({
395
400
  item: payload,
396
401
  props: { screenType: "hooks" },
397
402
  options: { ModalContainer: hookPlugin.getModalContainer() },
@@ -411,7 +416,7 @@ export function NavigationProvider({ children }: Props) {
411
416
  }
412
417
 
413
418
  if (hookPlugin.isModalHook()) {
414
- navigator.dismissModal();
419
+ dismissModal();
415
420
  } else {
416
421
  goBack(true, true);
417
422
  }
@@ -423,7 +428,7 @@ export function NavigationProvider({ children }: Props) {
423
428
  closeLoadingOverlay();
424
429
  };
425
430
 
426
- const handleHookCancel = ({ hookPlugin, callback }, dispose) => {
431
+ const handleHookCancel = ({ hookPlugin, callback, abort }, dispose) => {
427
432
  dispose();
428
433
  setStartUpHooks(false);
429
434
 
@@ -431,10 +436,12 @@ export function NavigationProvider({ children }: Props) {
431
436
  callback();
432
437
  }
433
438
 
434
- if (hookPlugin.isModalHook()) {
435
- navigator.dismissModal();
436
- } else {
437
- goBack(true, true);
439
+ if (!abort) {
440
+ if (hookPlugin.isModalHook()) {
441
+ dismissModal();
442
+ } else {
443
+ goBack(true, true);
444
+ }
438
445
  }
439
446
 
440
447
  closeLoadingOverlay();
@@ -444,7 +451,7 @@ export function NavigationProvider({ children }: Props) {
444
451
  callback?.();
445
452
 
446
453
  if (hookPlugin.isModalHook()) {
447
- navigator.dismissModal();
454
+ dismissModal();
448
455
  }
449
456
  };
450
457
 
@@ -681,33 +688,6 @@ export function NavigationProvider({ children }: Props) {
681
688
  }
682
689
  }, [state?.options?.videoModal?.previousMode]);
683
690
 
684
- const openModalBottomSheet = React.useCallback(
685
- ({
686
- ModalBottomSheetContent,
687
- modalBottomSheetContentProps,
688
- props = {},
689
- options = {},
690
- }) => {
691
- openModal({
692
- item: {
693
- ModalBottomSheetContent,
694
- modalBottomSheetContentProps,
695
- },
696
- props,
697
- options: {
698
- animationType: "none",
699
- animated: false,
700
- ...options,
701
- },
702
- });
703
- },
704
- [openModal]
705
- );
706
-
707
- const closeModalBottomSheet = React.useCallback(() => {
708
- dismissModal();
709
- }, [dismissModal]);
710
-
711
691
  const getNestedEntry = () => state?.location?.state?.nested?.entry ?? null;
712
692
 
713
693
  // It will only work for the regular stack and pathname, It won't work for modals
@@ -748,8 +728,6 @@ export function NavigationProvider({ children }: Props) {
748
728
  openModal,
749
729
  dismissModal,
750
730
  getModalState,
751
- openModalBottomSheet,
752
- closeModalBottomSheet,
753
731
  videoModalState: state.options.videoModal,
754
732
  openVideoModal,
755
733
  closeVideoModal,
@@ -766,7 +744,15 @@ export function NavigationProvider({ children }: Props) {
766
744
  getStackForPathname,
767
745
  mainStack: state.stack.mainStack,
768
746
  }),
769
- [rivers, plugins, JSON.stringify(state), appLaunched, startUpHooks]
747
+ [
748
+ rivers,
749
+ plugins,
750
+ // TODO: Compare entire screen data from state must be removed
751
+ // State size could be large ~150kb and take 80ms on android tv on 8 rails
752
+ JSON.stringify(state, getJsonReplacer()),
753
+ appLaunched,
754
+ startUpHooks,
755
+ ]
770
756
  );
771
757
 
772
758
  // when running the tests, we pass the dispatch function
@@ -461,48 +461,4 @@ describe("<NavigationProvider />", () => {
461
461
  });
462
462
  });
463
463
  });
464
-
465
- describe("modal feature", () => {
466
- it("sets the modalState when opening a modal", () => {
467
- act(() => {
468
- wrapper = renderContextConsumer();
469
- });
470
-
471
- const view = getByTestId(wrapper, "WrapperView");
472
- act(() => view.props.navigator.openModal({ item: rivers.A1234 }));
473
- expect(view.props.navigator.get).toMatchSnapshot();
474
- });
475
-
476
- it("removes the visible flag when dismissing a modal", () => {
477
- act(() => {
478
- wrapper = renderContextConsumer();
479
- });
480
-
481
- const view = getByTestId(wrapper, "WrapperView");
482
- act(() => view.props.navigator.openModal({ item: rivers.A1234 }));
483
- expect(view.props.navigator.getModalState()).toMatchSnapshot();
484
- act(() => view.props.navigator.dismissModal());
485
- expect(view.props.navigator.getModalState().visible).toBeFalsy();
486
- expect(view.props.navigator.getModalState()).toMatchSnapshot();
487
- });
488
-
489
- it("should reset state to initial when dismissing a modal ( closeModalBottomSheet ) opened by openModalBottomSheet", () => {
490
- act(() => {
491
- wrapper = renderContextConsumer();
492
- });
493
-
494
- const view = getByTestId(wrapper, "WrapperView");
495
-
496
- act(() =>
497
- view.props.navigator.openModalBottomSheet({
498
- ModalBottomSheetContent: rivers.A1234,
499
- modalBottomSheetContentProps: { some: "props" },
500
- })
501
- );
502
-
503
- expect(view.props.navigator.getModalState()).toMatchSnapshot();
504
- act(() => view.props.navigator.closeModalBottomSheet());
505
- expect(view.props.navigator.getModalState()).toMatchSnapshot();
506
- });
507
- });
508
464
  });
@@ -1,4 +1,3 @@
1
- /// <reference types="@types/tizen-tv-webapis" />
2
1
  import * as R from "ramda";
3
2
 
4
3
  import { NetInfoState } from "@react-native-community/netinfo";
@@ -0,0 +1,146 @@
1
+ import * as React from "react";
2
+ import { View, Text } from "react-native";
3
+ import { render, waitFor } from "@testing-library/react-native";
4
+ import thunk from "redux-thunk";
5
+ import configureStore from "redux-mock-store";
6
+
7
+ let mocked_items: Record<string, any> = {};
8
+
9
+ import { appLifeCycleManager } from "../";
10
+
11
+ const mock_actions = {};
12
+
13
+ jest.mock("@applicaster/zapp-react-native-redux/AppData", () => ({
14
+ loadAppData: jest.fn(() => () => Promise.resolve()),
15
+ }));
16
+
17
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/actions", () => ({
18
+ useActions: jest.fn(() => ({ actions: mock_actions })),
19
+ }));
20
+
21
+ jest.mock(
22
+ "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage",
23
+ () => ({
24
+ localStorage: {
25
+ getAllItems: jest.fn(() => Promise.resolve(mocked_items)),
26
+ setItem: jest.fn((key, value) => {
27
+ mocked_items[key] = JSON.stringify(value);
28
+
29
+ return Promise.resolve(true);
30
+ }),
31
+ getItem: jest.fn((key) => {
32
+ if (mocked_items[key]) {
33
+ if (typeof mocked_items[key] === "string") {
34
+ return Promise.resolve(JSON.parse(mocked_items[key]));
35
+ } else {
36
+ return Promise.resolve(mocked_items[key]);
37
+ }
38
+ } else {
39
+ return Promise.resolve([]);
40
+ }
41
+ }),
42
+ __reset() {
43
+ mocked_items = {};
44
+ },
45
+ },
46
+ })
47
+ );
48
+
49
+ const Component = () => (
50
+ <View>
51
+ <Text>I'm a component</Text>
52
+ </View>
53
+ );
54
+
55
+ const QuickBrickManager = {
56
+ sendQuickBrickEvent: jest.fn(),
57
+ };
58
+
59
+ const LOADING_ERROR = new Error("loading failed");
60
+
61
+ const appLoader = jest.fn((_dispatch, _getState) => {
62
+ if (_getState().shouldFail) return Promise.reject(LOADING_ERROR);
63
+
64
+ return Promise.resolve();
65
+ });
66
+
67
+ const store = configureStore([thunk])({});
68
+ const dispatch = store.dispatch;
69
+
70
+ const state = {
71
+ rivers: {},
72
+ plugins: {},
73
+ appSettings: { runtimeConfigurationUrls: { rivers: "http://rivers.url" } },
74
+ };
75
+
76
+ const QUICK_BRICK_READY_EVENT = "quickBrickReady";
77
+
78
+ describe("appLifeCycleManager", () => {
79
+ const decorator = appLifeCycleManager(appLoader, QuickBrickManager);
80
+ const DecoratedComponent = decorator(Component);
81
+
82
+ beforeEach(() => {
83
+ appLoader.mockClear();
84
+ QuickBrickManager.sendQuickBrickEvent.mockClear();
85
+ });
86
+
87
+ it("returns a function to decorate a component", () => {
88
+ expect(decorator).toBeDefined();
89
+ });
90
+
91
+ it("runs the loaders, then sends quickBrickReady event", async () => {
92
+ const testCallback = jest.fn(() => {});
93
+
94
+ render(
95
+ <DecoratedComponent
96
+ dispatch={dispatch}
97
+ {...state}
98
+ testCallback={testCallback}
99
+ />
100
+ );
101
+
102
+ await waitFor(() => {
103
+ return expect(testCallback).toHaveBeenCalled();
104
+ });
105
+
106
+ const actions = store.getActions();
107
+
108
+ expect(actions[0]).toMatchObject({ type: "SET_APP_READY" });
109
+
110
+ expect(QuickBrickManager.sendQuickBrickEvent).toHaveBeenCalledWith(
111
+ QUICK_BRICK_READY_EVENT
112
+ );
113
+
114
+ expect(appLoader).toHaveBeenCalled();
115
+ });
116
+
117
+ it("sends the quickBrickReady event when a loader doesn't resolves", async () => {
118
+ const testCallback = jest.fn(() => {});
119
+
120
+ render(
121
+ <DecoratedComponent
122
+ dispatch={dispatch}
123
+ {...state}
124
+ shouldFail
125
+ testCallback={testCallback}
126
+ />
127
+ );
128
+
129
+ const actions = store.getActions();
130
+
131
+ await waitFor(() => {
132
+ return expect(testCallback).toHaveBeenCalled();
133
+ });
134
+
135
+ expect(actions[0]).toMatchObject({ type: "SET_APP_READY" });
136
+ expect(actions[1]).toMatchObject({ type: "SET_APP_LAUNCHED" });
137
+
138
+ expect(QuickBrickManager.sendQuickBrickEvent).toHaveBeenCalledWith(
139
+ QUICK_BRICK_READY_EVENT
140
+ );
141
+
142
+ expect(testCallback).toHaveBeenCalled();
143
+
144
+ expect(appLoader).toHaveBeenCalled();
145
+ });
146
+ });
@@ -182,13 +182,13 @@ export function appLifeCycleManager(appLoader, QuickBrickManager) {
182
182
  data: { error },
183
183
  });
184
184
  }
185
-
186
- makeAppReady();
187
- checkStoredContextKeys();
188
185
  };
189
186
 
190
187
  React.useLayoutEffect(() => {
191
- prepareApp();
188
+ prepareApp().then(() => {
189
+ makeAppReady();
190
+ checkStoredContextKeys();
191
+ });
192
192
  }, []);
193
193
 
194
194
  return <Component {...props} />;
package/App/index.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as React from "react";
2
2
  import * as R from "ramda";
3
+
3
4
  import {
4
5
  initialWindowMetrics,
5
6
  SafeAreaProvider,
@@ -72,11 +73,11 @@ const App = ({
72
73
  </SplashLoader>
73
74
  <ModalProvider />
74
75
  </URLSchemeListener>
75
- <LogicAggregatorContainer
76
- components={[InteractionManager, ApplyScreenHooks]}
77
- />
78
76
  <LoadingOverlay />
79
77
  </ErrorBoundary>
78
+ <LogicAggregatorContainer
79
+ components={[InteractionManager, ApplyScreenHooks]}
80
+ />
80
81
  </AppContainer>
81
82
  </SafeAreaProvider>
82
83
  </ThemeManager>
@@ -1,4 +1,5 @@
1
1
  import * as R from "ramda";
2
+ import isEmpty from "lodash/isEmpty";
2
3
  import { Dimensions, NativeModules } from "react-native";
3
4
  import { loadAppContextData } from "@applicaster/zapp-react-native-redux";
4
5
  import { isTablet } from "@applicaster/zapp-react-native-utils/reactHooks";
@@ -10,6 +11,7 @@ import { logger } from "@applicaster/zapp-react-native-utils/logger";
10
11
  import { ACTIVE_LAYOUT_ID_STORAGE_KEY } from "../consts";
11
12
  const { AppLoaderBridge } = NativeModules;
12
13
  import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage";
14
+ import { useErrorStore } from "../../ErrorBoundary/store";
13
15
 
14
16
  export async function getNativeRemoteContextData(
15
17
  dispatch,
@@ -58,6 +60,18 @@ export async function getNativeRemoteContextData(
58
60
 
59
61
  const layout = await getPromiseForType("layout");
60
62
 
63
+ const layoutLoadingErrorMessage = "Failed to load layout data";
64
+
65
+ if (isEmpty(layout)) {
66
+ useErrorStore
67
+ .getState()
68
+ .setError(
69
+ new Error(layoutLoadingErrorMessage),
70
+ layoutLoadingErrorMessage,
71
+ false
72
+ );
73
+ }
74
+
61
75
  const apiVersion = layout?.layout?.["api_version"]; // Taken from remote configuration
62
76
 
63
77
  dispatch(appDataActions.merge({ layoutVersion: apiVersion || "v1" }));
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "@applicaster/quick-brick-core",
3
- "version": "13.0.0-alpha.3962361544",
3
+ "version": "13.0.0-alpha.4012243505",
4
4
  "description": "Core package for Applicaster's Quick Brick App",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
7
  "scripts": {
8
- "test": "jest -w=2",
9
8
  "build": "echo 'skipping ts build for core packages'"
10
9
  },
11
10
  "repository": {
@@ -29,13 +28,13 @@
29
28
  },
30
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
31
30
  "dependencies": {
32
- "@applicaster/applicaster-types": "13.0.0-alpha.3962361544",
33
- "@applicaster/quick-brick-core-plugins": "13.0.0-alpha.3962361544",
34
- "@applicaster/zapp-pipes-v2-client": "13.0.0-alpha.3962361544",
35
- "@applicaster/zapp-react-native-bridge": "13.0.0-alpha.3962361544",
36
- "@applicaster/zapp-react-native-redux": "13.0.0-alpha.3962361544",
37
- "@applicaster/zapp-react-native-ui-components": "13.0.0-alpha.3962361544",
38
- "@applicaster/zapp-react-native-utils": "13.0.0-alpha.3962361544",
31
+ "@applicaster/applicaster-types": "13.0.0-alpha.4012243505",
32
+ "@applicaster/quick-brick-core-plugins": "13.0.0-alpha.4012243505",
33
+ "@applicaster/zapp-pipes-v2-client": "13.0.0-alpha.4012243505",
34
+ "@applicaster/zapp-react-native-bridge": "13.0.0-alpha.4012243505",
35
+ "@applicaster/zapp-react-native-redux": "13.0.0-alpha.4012243505",
36
+ "@applicaster/zapp-react-native-ui-components": "13.0.0-alpha.4012243505",
37
+ "@applicaster/zapp-react-native-utils": "13.0.0-alpha.4012243505",
39
38
  "atob": "^2.1.2",
40
39
  "axios": "^0.28.0",
41
40
  "btoa": "^1.2.1",
@@ -57,7 +56,6 @@
57
56
  "@applicaster/zapp-react-native-tvos-ui-components": "*",
58
57
  "@babel/runtime": "*",
59
58
  "@react-native-community/netinfo": "*",
60
- "@types/node": "*",
61
59
  "immer": "*",
62
60
  "react": "*",
63
61
  "react-native": "*",
@@ -73,8 +71,6 @@
73
71
  "zustand": "*"
74
72
  },
75
73
  "devDependencies": {
76
- "@types/react-native": "0.69.6",
77
- "@types/tizen-tv-webapis": "^2.0.1",
78
74
  "nock": "^10.0.0",
79
75
  "redux-mock-store": "^1.5.3"
80
76
  },
@@ -1,55 +0,0 @@
1
- import React from "react";
2
- import { View, Text } from "react-native";
3
- import { render } from "@testing-library/react-native";
4
-
5
- import { ErrorBoundary } from "../index";
6
-
7
- const TestComponent = ({ error }: { error: any }) => {
8
- if (error) {
9
- throw new Error("error !!");
10
- }
11
-
12
- return (
13
- <View>
14
- <Text>all is good</Text>
15
- </View>
16
- );
17
- };
18
-
19
- describe("<ErrorBoundary />", () => {
20
- describe("when there is no error", () => {
21
- it("renders correctly", () => {
22
- const { getByText } = render(
23
- <ErrorBoundary>
24
- <TestComponent />
25
- </ErrorBoundary>
26
- );
27
-
28
- expect(getByText("all is good")).toBeTruthy();
29
- });
30
- });
31
-
32
- describe("When an error is caught", () => {
33
- const _consoleLog = console.log;
34
-
35
- beforeAll(() => {
36
- console.log = jest.fn();
37
- });
38
-
39
- afterAll(() => {
40
- console.log = _consoleLog;
41
- });
42
-
43
- it("renders error state correctly", () => {
44
- const { getByText } = render(
45
- <ErrorBoundary>
46
- <TestComponent error />
47
- </ErrorBoundary>
48
- );
49
-
50
- // Note: You'll need to verify what your ErrorBoundary actually renders in error state
51
- // This is just an example assertion:
52
- expect(getByText(/error/i)).toBeTruthy();
53
- });
54
- });
55
- });
@@ -1 +0,0 @@
1
- export { Fragment as ModalChildrenWrapper } from "react";
@@ -1,68 +0,0 @@
1
- // Jest Snapshot v1, https://goo.gl/fbAQLP
2
-
3
- exports[`<NavigationProvider /> modal feature removes the visible flag when dismissing a modal 1`] = `
4
- {
5
- "options": {
6
- "animated": true,
7
- "animationType": "slide",
8
- "onDismiss": [Function],
9
- "onRequestClose": [Function],
10
- "onShow": [Function],
11
- "presentationStyle": "overFullScreen",
12
- "transparent": true,
13
- },
14
- "props": undefined,
15
- "screen": {
16
- "id": "A1234",
17
- "navigations": [],
18
- "ui_components": [],
19
- },
20
- "visible": true,
21
- }
22
- `;
23
-
24
- exports[`<NavigationProvider /> modal feature removes the visible flag when dismissing a modal 2`] = `
25
- {
26
- "options": {},
27
- "props": {},
28
- "screen": null,
29
- "visible": false,
30
- }
31
- `;
32
-
33
- exports[`<NavigationProvider /> modal feature sets the modalState when opening a modal 1`] = `undefined`;
34
-
35
- exports[`<NavigationProvider /> modal feature should reset state to initial when dismissing a modal ( closeModalBottomSheet ) opened by openModalBottomSheet 1`] = `
36
- {
37
- "options": {
38
- "animated": false,
39
- "animationType": "none",
40
- "onDismiss": [Function],
41
- "onRequestClose": [Function],
42
- "onShow": [Function],
43
- "presentationStyle": "overFullScreen",
44
- "transparent": true,
45
- },
46
- "props": {},
47
- "screen": {
48
- "ModalBottomSheetContent": {
49
- "id": "A1234",
50
- "navigations": [],
51
- "ui_components": [],
52
- },
53
- "modalBottomSheetContentProps": {
54
- "some": "props",
55
- },
56
- },
57
- "visible": true,
58
- }
59
- `;
60
-
61
- exports[`<NavigationProvider /> modal feature should reset state to initial when dismissing a modal ( closeModalBottomSheet ) opened by openModalBottomSheet 2`] = `
62
- {
63
- "options": {},
64
- "props": {},
65
- "screen": null,
66
- "visible": false,
67
- }
68
- `;
@@ -1,30 +0,0 @@
1
- // Jest Snapshot v1, https://goo.gl/fbAQLP
2
-
3
- exports[`appLifeCycleManager runs the loaders, then sends quickBrickReady event 1`] = `
4
- <View>
5
- <Text>
6
- I'm a component
7
- </Text>
8
- </View>
9
- `;
10
-
11
- exports[`appLifeCycleManager sends the quickBrickReady event when a loader doesn't resolves 1`] = `
12
- <View>
13
- <Text>
14
- I'm a component
15
- </Text>
16
- </View>
17
- `;
18
-
19
- exports[`appLifeCycleManager sends the quickBrickReady event when a loader doesn't resolves 2`] = `
20
- [
21
- [
22
- {
23
- "data": {
24
- "error": [Error: loading failed],
25
- },
26
- "message": "AppLifeCycleManager: A loading operation failed: loading failed",
27
- },
28
- ],
29
- ]
30
- `;
@@ -1,134 +0,0 @@
1
- import * as React from "react";
2
- import { View, Text } from "react-native";
3
- import { create, act } from "react-test-renderer";
4
-
5
- const Component = () => (
6
- <View>
7
- <Text>I'm a component</Text>
8
- </View>
9
- );
10
-
11
- const mockLogger = {
12
- warning: jest.fn(),
13
- log: jest.fn(),
14
- debug: jest.fn(),
15
- info: jest.fn(),
16
- };
17
-
18
- jest.mock("../../logger", () => ({
19
- coreAppLogger: {
20
- addSubsystem: jest.fn(() => mockLogger),
21
- },
22
- }));
23
-
24
- const QuickBrickManager = {
25
- sendQuickBrickEvent: jest.fn(),
26
- };
27
-
28
- const LOADING_ERROR = new Error("loading failed");
29
-
30
- const appLoader = jest.fn((_dispatch, _getState) => {
31
- if (_getState().shouldFail) return Promise.reject(LOADING_ERROR);
32
-
33
- return Promise.resolve();
34
- });
35
-
36
- const dispatch = jest.fn();
37
-
38
- const state = {
39
- rivers: {},
40
- plugins: {},
41
- appSettings: { runtimeConfigurationUrls: { rivers: "http://rivers.url" } },
42
- };
43
-
44
- const QUICK_BRICK_READY_EVENT = "quickBrickReady";
45
-
46
- const actions = {};
47
-
48
- jest.doMock("@applicaster/zapp-react-native-utils/reactHooks/actions", () => ({
49
- useActions: jest.fn(() => ({ actions })),
50
- }));
51
-
52
- const { appLifeCycleManager } = require("../index");
53
-
54
- describe("appLifeCycleManager", () => {
55
- const decorator = appLifeCycleManager(appLoader, QuickBrickManager);
56
-
57
- beforeEach(() => {
58
- mockLogger.warning.mockClear();
59
- appLoader.mockClear();
60
- QuickBrickManager.sendQuickBrickEvent.mockClear();
61
- dispatch.mockClear();
62
- });
63
-
64
- it("returns a function to decorate a component", () => {
65
- expect(decorator).toBeFunction();
66
- });
67
-
68
- it("runs the loaders, then sends quickBrickReady event", (done) => {
69
- const DecoratedComponent = decorator(Component);
70
-
71
- let wrapper;
72
-
73
- const testCallback = () => {
74
- expect(dispatch).toHaveBeenCalledWith(
75
- expect.objectContaining({ type: "SET_APP_READY" })
76
- );
77
-
78
- expect(QuickBrickManager.sendQuickBrickEvent).toHaveBeenCalledWith(
79
- QUICK_BRICK_READY_EVENT
80
- );
81
-
82
- done();
83
- };
84
-
85
- act(() => {
86
- wrapper = create(
87
- <DecoratedComponent
88
- dispatch={dispatch}
89
- {...state}
90
- testCallback={testCallback}
91
- />
92
- );
93
- });
94
-
95
- expect(wrapper.toJSON()).toMatchSnapshot();
96
- expect(appLoader).toHaveBeenCalled();
97
- expect.assertions(4);
98
- });
99
-
100
- it("sends the quickBrickReady event when a loader doesn't resolves", (done) => {
101
- const DecoratedComponent = decorator(Component);
102
-
103
- let wrapper;
104
-
105
- const testCallback = () => {
106
- expect(dispatch).toHaveBeenCalledWith(
107
- expect.objectContaining({ type: "SET_APP_READY" })
108
- );
109
-
110
- expect(QuickBrickManager.sendQuickBrickEvent).toHaveBeenCalledWith(
111
- QUICK_BRICK_READY_EVENT
112
- );
113
-
114
- expect(mockLogger.warning.mock.calls).toMatchSnapshot();
115
-
116
- done();
117
- };
118
-
119
- act(() => {
120
- wrapper = create(
121
- <DecoratedComponent
122
- dispatch={dispatch}
123
- {...state}
124
- shouldFail
125
- testCallback={testCallback}
126
- />
127
- );
128
- });
129
-
130
- expect(wrapper.toJSON()).toMatchSnapshot();
131
- expect(appLoader).toHaveBeenCalled();
132
- expect.assertions(5);
133
- });
134
- });