@applicaster/zapp-react-native-ui-components 14.0.21 → 14.0.22-alpha.4201433163
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Components/GeneralContentScreen/GeneralContentScreen.tsx +35 -19
- package/Components/GeneralContentScreen/__tests__/GeneralContentScreen.test.tsx +104 -0
- package/Components/GeneralContentScreen/utils/__tests__/getScreenDataSource.test.ts +19 -0
- package/Components/GeneralContentScreen/utils/getScreenDataSource.ts +9 -0
- package/Components/HookRenderer/HookRenderer.tsx +38 -3
- package/Components/HookRenderer/__tests__/HookRenderer.test.tsx +60 -0
- package/Components/PreloaderWrapper/__tests__/index.test.tsx +26 -0
- package/Components/PreloaderWrapper/index.tsx +15 -0
- package/Components/ScreenFeedLoader/ScreenFeedLoader.tsx +46 -0
- package/Components/ScreenFeedLoader/__tests__/ScreenFeedLoader.test.tsx +94 -0
- package/Components/ScreenFeedLoader/index.ts +1 -0
- package/Components/ScreenResolver/__tests__/screenResolver.test.js +24 -0
- package/Components/ScreenResolver/hooks/index.ts +3 -0
- package/Components/ScreenResolver/hooks/useGetComponent.ts +15 -0
- package/Components/ScreenResolver/hooks/useScreenComponentResolver.tsx +90 -0
- package/Components/ScreenResolver/index.tsx +15 -93
- package/Components/ScreenResolver/utils/__tests__/getScreenTypeProps.test.ts +45 -0
- package/Components/ScreenResolver/utils/getScreenTypeProps.ts +43 -0
- package/Components/ScreenResolver/utils/index.ts +1 -0
- package/Components/ScreenResolver/withDefaultScreenContext.tsx +16 -0
- package/Components/ScreenResolverFeedProvider/ScreenResolverFeedProvider.tsx +25 -0
- package/Components/ScreenResolverFeedProvider/__tests__/ScreenResolverFeedProvider.test.tsx +44 -0
- package/Components/ScreenResolverFeedProvider/index.ts +1 -0
- package/Components/index.js +1 -1
- package/Contexts/ScreenContext/__tests__/index.test.tsx +57 -0
- package/Contexts/ScreenContext/index.tsx +17 -0
- package/Contexts/ZappPipesContext/ZappPipesContextFactory.tsx +18 -7
- package/package.json +5 -5
- /package/Components/HookRenderer/{index.tsx → index.ts} +0 -0
|
@@ -12,12 +12,26 @@ import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
|
12
12
|
import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
|
|
13
13
|
import { ScreenTrackedViewPositionsContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenTrackedViewPositionsContext";
|
|
14
14
|
import { useEventAlerts } from "./utils/useEventAlerts";
|
|
15
|
+
import {
|
|
16
|
+
selectRiverById,
|
|
17
|
+
useAppSelector,
|
|
18
|
+
} from "@applicaster/zapp-react-native-redux";
|
|
19
|
+
import { getScreenDataSource } from "./utils/getScreenDataSource";
|
|
20
|
+
import { ScreenResolverFeedProvider } from "../ScreenResolverFeedProvider/ScreenResolverFeedProvider";
|
|
15
21
|
|
|
16
22
|
const { log_debug } = createLogger({
|
|
17
23
|
category: "ScreenContainer",
|
|
18
24
|
subsystem: "General",
|
|
19
25
|
});
|
|
20
26
|
|
|
27
|
+
/** Provides screen-feed from general-screen configuration (if defined) */
|
|
28
|
+
const useFeedData = (id) => {
|
|
29
|
+
const river = useAppSelector((state) => selectRiverById(state, id));
|
|
30
|
+
const feedData = getScreenDataSource(river);
|
|
31
|
+
|
|
32
|
+
return feedData;
|
|
33
|
+
};
|
|
34
|
+
|
|
21
35
|
export const GeneralContentScreen = ({
|
|
22
36
|
feed,
|
|
23
37
|
screenId,
|
|
@@ -103,24 +117,26 @@ export const GeneralContentScreen = ({
|
|
|
103
117
|
if (!isReady || isNilOrEmpty(components || uiComponents)) return null;
|
|
104
118
|
|
|
105
119
|
return (
|
|
106
|
-
<
|
|
107
|
-
<
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
<ScreenResolverFeedProvider id={screenId} useFeedData={useFeedData}>
|
|
121
|
+
<ScreenTrackedViewPositionsContext.Provider>
|
|
122
|
+
<CellTapContext.Provider value={contextValue}>
|
|
123
|
+
<ComponentsMap
|
|
124
|
+
feed={feed}
|
|
125
|
+
riverId={screenId}
|
|
126
|
+
groupId={groupId || `general-content-screen-${screenId}`}
|
|
127
|
+
riverComponents={components || uiComponents}
|
|
128
|
+
scrollViewExtraProps={scrollViewExtraProps}
|
|
129
|
+
getStaticComponentFeed={getStaticComponentFeed}
|
|
130
|
+
extraAnchorPointYOffset={extraAnchorPointYOffset}
|
|
131
|
+
isScreenWrappedInContainer={isScreenWrappedInContainer}
|
|
132
|
+
parentFocus={parentFocus}
|
|
133
|
+
focused={focused}
|
|
134
|
+
containerHeight={containerHeight}
|
|
135
|
+
preferredFocus={preferredFocus}
|
|
136
|
+
{...componentsMapExtraProps}
|
|
137
|
+
/>
|
|
138
|
+
</CellTapContext.Provider>
|
|
139
|
+
</ScreenTrackedViewPositionsContext.Provider>
|
|
140
|
+
</ScreenResolverFeedProvider>
|
|
125
141
|
);
|
|
126
142
|
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "@testing-library/react-native";
|
|
3
|
+
import { GeneralContentScreen } from "../GeneralContentScreen";
|
|
4
|
+
|
|
5
|
+
const mockUseAppSelector = jest.fn();
|
|
6
|
+
const mockSelectRiverById = jest.fn();
|
|
7
|
+
const mockProviderSpy = jest.fn();
|
|
8
|
+
|
|
9
|
+
jest.mock("../../River/ComponentsMap", () => ({
|
|
10
|
+
ComponentsMap: () => {
|
|
11
|
+
const React = require("react");
|
|
12
|
+
const { View } = require("react-native");
|
|
13
|
+
|
|
14
|
+
return <View testID="components-map" />;
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
jest.mock("@applicaster/zapp-react-native-utils/reactHooks/actions", () => ({
|
|
19
|
+
useActions: jest.fn(() => jest.fn()),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
jest.mock("../utils", () => ({
|
|
23
|
+
logger: { warn: jest.fn() },
|
|
24
|
+
whenMatchingType: jest.fn((_type, value) => value),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
jest.mock("@applicaster/zapp-react-native-utils/reactHooks/layout", () => ({
|
|
28
|
+
useLayoutVersion: jest.fn(() => false),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.mock(
|
|
32
|
+
"@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenData",
|
|
33
|
+
() => ({
|
|
34
|
+
useScreenData: jest.fn(() => ({
|
|
35
|
+
ui_components: [{ id: "ui-component" }],
|
|
36
|
+
general: {},
|
|
37
|
+
})),
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
jest.mock("../utils/useCurationAPI", () => ({
|
|
42
|
+
useCurationAPI: jest.fn(() => [{ id: "curation-component" }]),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
jest.mock("@applicaster/quick-brick-core/App/ActionSetters", () => ({
|
|
46
|
+
useRiverInitialState: jest.fn(() => []),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
jest.mock("../utils/useEventAlerts", () => ({
|
|
50
|
+
useEventAlerts: jest.fn(),
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
jest.mock("@applicaster/zapp-react-native-redux", () => ({
|
|
54
|
+
useAppSelector: (...args) => mockUseAppSelector(...args),
|
|
55
|
+
selectRiverById: (...args) => mockSelectRiverById(...args),
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
jest.mock("../utils/getScreenDataSource", () => ({
|
|
59
|
+
getScreenDataSource: jest.fn(() => ({
|
|
60
|
+
source: "https://feed",
|
|
61
|
+
mapping: {},
|
|
62
|
+
})),
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
jest.mock(
|
|
66
|
+
"../../ScreenResolverFeedProvider/ScreenResolverFeedProvider",
|
|
67
|
+
() => ({
|
|
68
|
+
ScreenResolverFeedProvider: ({ id, useFeedData, children }) => {
|
|
69
|
+
const React = require("react");
|
|
70
|
+
const { View } = require("react-native");
|
|
71
|
+
|
|
72
|
+
mockProviderSpy(id, useFeedData);
|
|
73
|
+
useFeedData(id);
|
|
74
|
+
|
|
75
|
+
return <View testID="screen-resolver-feed-provider">{children}</View>;
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
describe("GeneralContentScreen", () => {
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
jest.clearAllMocks();
|
|
83
|
+
mockUseAppSelector.mockImplementation((selector) => selector({}));
|
|
84
|
+
mockSelectRiverById.mockReturnValue({ id: "screen-1" });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("wraps content with ScreenResolverFeedProvider and renders ComponentsMap", () => {
|
|
88
|
+
const { getByTestId } = render(
|
|
89
|
+
<GeneralContentScreen
|
|
90
|
+
screenId="screen-1"
|
|
91
|
+
feed={null}
|
|
92
|
+
components={[{ id: "component-1" }]}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(getByTestId("screen-resolver-feed-provider")).toBeDefined();
|
|
97
|
+
expect(getByTestId("components-map")).toBeDefined();
|
|
98
|
+
|
|
99
|
+
expect(mockProviderSpy).toHaveBeenCalledWith(
|
|
100
|
+
"screen-1",
|
|
101
|
+
expect.any(Function)
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getScreenDataSource } from "../getScreenDataSource";
|
|
2
|
+
|
|
3
|
+
describe("getScreenDataSource", () => {
|
|
4
|
+
it("returns screen_feed data when present", () => {
|
|
5
|
+
const result = getScreenDataSource({
|
|
6
|
+
data: {
|
|
7
|
+
screen_feed: {
|
|
8
|
+
source: "https://feed",
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
expect(result).toEqual({ source: "https://feed" });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("returns undefined when screen_feed is missing", () => {
|
|
17
|
+
expect(getScreenDataSource({ data: {} })).toBeUndefined();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { get } from "@applicaster/zapp-react-native-utils/utils";
|
|
2
|
+
|
|
3
|
+
const lookupPath = ["data", "screen_feed"];
|
|
4
|
+
|
|
5
|
+
export const getScreenDataSource = (
|
|
6
|
+
screenData: any
|
|
7
|
+
): Option<ZappDataSource> => {
|
|
8
|
+
return get(screenData, lookupPath) as ZappDataSource | undefined;
|
|
9
|
+
};
|
|
@@ -7,25 +7,31 @@ import {
|
|
|
7
7
|
import { useHookAnalytics } from "@applicaster/zapp-react-native-utils/analyticsUtils/helpers/hooks";
|
|
8
8
|
import { useSetNavbarState } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
9
9
|
|
|
10
|
+
import { componentsLogger } from "../../Helpers/logger";
|
|
11
|
+
|
|
12
|
+
const logger = componentsLogger.addSubsystem("HookRenderer");
|
|
13
|
+
|
|
14
|
+
const HOOK_PRESENTATION_TYPE = "Hook";
|
|
15
|
+
|
|
10
16
|
type Props = {
|
|
11
17
|
focused?: boolean;
|
|
12
18
|
parentFocus?: ParentFocus;
|
|
13
19
|
screenData: HookPluginProps;
|
|
14
20
|
};
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
const HookRenderer = (props: Props) => {
|
|
17
23
|
const {
|
|
18
24
|
focused,
|
|
19
25
|
screenData: { callback, payload, hookPlugin },
|
|
20
26
|
} = props;
|
|
21
27
|
|
|
22
|
-
const { setVisible: showNavBar } = useSetNavbarState();
|
|
23
|
-
|
|
24
28
|
const {
|
|
25
29
|
module: { Component: HookComponent, presentFullScreen },
|
|
26
30
|
configuration,
|
|
27
31
|
} = hookPlugin;
|
|
28
32
|
|
|
33
|
+
const { setVisible: showNavBar } = useSetNavbarState();
|
|
34
|
+
|
|
29
35
|
useHookAnalytics(props);
|
|
30
36
|
|
|
31
37
|
const isNavBarVisible = useIsNavBarVisible();
|
|
@@ -60,7 +66,36 @@ export const HookRenderer = (props: Props) => {
|
|
|
60
66
|
hookPlugin,
|
|
61
67
|
focused,
|
|
62
68
|
parentFocus,
|
|
69
|
+
presentationType: HOOK_PRESENTATION_TYPE,
|
|
63
70
|
}}
|
|
64
71
|
/>
|
|
65
72
|
);
|
|
66
73
|
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Guard component to prevent rendering HookRenderer when screenData or hookPlugin is missing. This is to avoid potential crashes due to missing data.
|
|
77
|
+
*/
|
|
78
|
+
const HookRendererGuard = (props: Props) => {
|
|
79
|
+
React.useEffect(() => {
|
|
80
|
+
if (!props.screenData) {
|
|
81
|
+
logger.error(
|
|
82
|
+
"HookRenderer received no screenData, screen cannot be rendered"
|
|
83
|
+
);
|
|
84
|
+
} else if (!props.screenData.hookPlugin) {
|
|
85
|
+
logger.error(
|
|
86
|
+
"HookRenderer received screenData with no hookPlugin, screen cannot be rendered",
|
|
87
|
+
{
|
|
88
|
+
screenData: props.screenData,
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
}, [props.screenData]);
|
|
93
|
+
|
|
94
|
+
if (!props.screenData || !props.screenData.hookPlugin) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return <HookRenderer {...props} />;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export { HookRendererGuard as HookRenderer };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { render } from "@testing-library/react-native";
|
|
4
|
+
import { HookRenderer } from "..";
|
|
5
|
+
|
|
6
|
+
jest.mock("@applicaster/zapp-react-native-utils/reactUtils", () => ({
|
|
7
|
+
isTV: jest.fn(() => false),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
jest.mock("@applicaster/zapp-react-native-utils/reactHooks/navigation", () => ({
|
|
11
|
+
useBackHandler: jest.fn(),
|
|
12
|
+
useIsNavBarVisible: jest.fn(() => true),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
jest.mock(
|
|
16
|
+
"@applicaster/zapp-react-native-utils/analyticsUtils/helpers/hooks",
|
|
17
|
+
() => ({
|
|
18
|
+
useHookAnalytics: jest.fn(),
|
|
19
|
+
})
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
jest.mock("@applicaster/zapp-react-native-utils/reactHooks", () => ({
|
|
23
|
+
useSetNavbarState: jest.fn(() => ({
|
|
24
|
+
setVisible: jest.fn(),
|
|
25
|
+
})),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
describe("HookRenderer", () => {
|
|
29
|
+
it("returns null when hookPlugin is missing", () => {
|
|
30
|
+
const { toJSON } = render(
|
|
31
|
+
<HookRenderer callback={jest.fn()} screenData={{ payload: {} } as any} />
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
expect(toJSON()).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("passes Hook presentationType to rendered hook component", () => {
|
|
38
|
+
const HookComponent = (props) => (
|
|
39
|
+
<View testID="hook-component" {...props} />
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const { getByTestId } = render(
|
|
43
|
+
<HookRenderer
|
|
44
|
+
callback={jest.fn()}
|
|
45
|
+
screenData={{
|
|
46
|
+
payload: { foo: "bar" },
|
|
47
|
+
hookPlugin: {
|
|
48
|
+
module: {
|
|
49
|
+
Component: HookComponent,
|
|
50
|
+
presentFullScreen: false,
|
|
51
|
+
},
|
|
52
|
+
configuration: {},
|
|
53
|
+
},
|
|
54
|
+
}}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(getByTestId("hook-component").props.presentationType).toBe("Hook");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text } from "react-native";
|
|
3
|
+
import { render } from "@testing-library/react-native";
|
|
4
|
+
import { PreloaderWrapper } from "..";
|
|
5
|
+
|
|
6
|
+
describe("PreloaderWrapper", () => {
|
|
7
|
+
it("renders children when preloader is hidden", () => {
|
|
8
|
+
const { getByText } = render(
|
|
9
|
+
<PreloaderWrapper showPreloader={false}>
|
|
10
|
+
<Text>content</Text>
|
|
11
|
+
</PreloaderWrapper>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
expect(getByText("content")).toBeDefined();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("renders null when preloader is shown", () => {
|
|
18
|
+
const { queryByText } = render(
|
|
19
|
+
<PreloaderWrapper showPreloader>
|
|
20
|
+
<Text>content</Text>
|
|
21
|
+
</PreloaderWrapper>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(queryByText("content")).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
type PreloaderWrapperProps = {
|
|
4
|
+
showPreloader?: boolean;
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const PreloaderWrapper: React.FC<PreloaderWrapperProps> = ({
|
|
9
|
+
showPreloader = false,
|
|
10
|
+
children,
|
|
11
|
+
}) => {
|
|
12
|
+
return !showPreloader ? children : null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default PreloaderWrapper;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import { PreloaderWrapper } from "../PreloaderWrapper";
|
|
3
|
+
import { useScreenContextV2 } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
|
|
4
|
+
import { useFeedLoader } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
5
|
+
|
|
6
|
+
import { componentsLogger } from "../../Helpers/logger";
|
|
7
|
+
|
|
8
|
+
const logger = componentsLogger.addSubsystem("ScreenFeedLoader");
|
|
9
|
+
|
|
10
|
+
/** Loads and provides `feedData` and store to */
|
|
11
|
+
export const ScreenFeedLoader: React.FC<
|
|
12
|
+
React.PropsWithChildren<{ id: string; feedData: any }>
|
|
13
|
+
> = ({ id, feedData, children }) => {
|
|
14
|
+
const { source: feedUrl, mapping } = feedData;
|
|
15
|
+
|
|
16
|
+
const { data, loading, error } = useFeedLoader({
|
|
17
|
+
feedUrl,
|
|
18
|
+
mapping,
|
|
19
|
+
pipesOptions: {},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const feedStore = useScreenContextV2()._feedStore;
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (data && !loading) {
|
|
26
|
+
feedStore.setState({ screenFeed: data, screenFeedError: null });
|
|
27
|
+
|
|
28
|
+
logger.log("screenFeed set for active screen", { data, screenId: id });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (error && !loading) {
|
|
32
|
+
feedStore.setState({ screenFeed: data, screenFeedError: error });
|
|
33
|
+
|
|
34
|
+
logger.warning("Feed data error:", {
|
|
35
|
+
data,
|
|
36
|
+
loading,
|
|
37
|
+
error,
|
|
38
|
+
screenId: id,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}, [data, loading, error, feedStore, id]);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<PreloaderWrapper showPreloader={loading}>{children}</PreloaderWrapper>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text } from "react-native";
|
|
3
|
+
import { render, waitFor } from "@testing-library/react-native";
|
|
4
|
+
import { ScreenFeedLoader } from "../ScreenFeedLoader";
|
|
5
|
+
|
|
6
|
+
const mockUseFeedLoader = jest.fn();
|
|
7
|
+
const mockUseScreenContextV2 = jest.fn();
|
|
8
|
+
const mockSetState = jest.fn();
|
|
9
|
+
|
|
10
|
+
jest.mock("@applicaster/zapp-react-native-utils/reactHooks", () => ({
|
|
11
|
+
useFeedLoader: (...args) => mockUseFeedLoader(...args),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
jest.mock(
|
|
15
|
+
"@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext",
|
|
16
|
+
() => ({
|
|
17
|
+
useScreenContextV2: (...args) => mockUseScreenContextV2(...args),
|
|
18
|
+
})
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
describe("ScreenFeedLoader", () => {
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
|
|
25
|
+
mockUseScreenContextV2.mockReturnValue({
|
|
26
|
+
_feedStore: {
|
|
27
|
+
setState: mockSetState,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("hides children while loading", () => {
|
|
33
|
+
mockUseFeedLoader.mockReturnValue({
|
|
34
|
+
data: null,
|
|
35
|
+
loading: true,
|
|
36
|
+
error: null,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const { queryByText } = render(
|
|
40
|
+
<ScreenFeedLoader id="test" feedData={{ source: "url", mapping: {} }}>
|
|
41
|
+
<Text>child</Text>
|
|
42
|
+
</ScreenFeedLoader>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
expect(queryByText("child")).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("writes loaded feed data to _feedStore", async () => {
|
|
49
|
+
const data = { entry: { id: "1" } };
|
|
50
|
+
|
|
51
|
+
mockUseFeedLoader.mockReturnValue({
|
|
52
|
+
data,
|
|
53
|
+
loading: false,
|
|
54
|
+
error: null,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
render(
|
|
58
|
+
<ScreenFeedLoader id="test" feedData={{ source: "url", mapping: {} }}>
|
|
59
|
+
<Text>child</Text>
|
|
60
|
+
</ScreenFeedLoader>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
await waitFor(() => {
|
|
64
|
+
expect(mockSetState).toHaveBeenCalledWith({
|
|
65
|
+
screenFeed: data,
|
|
66
|
+
screenFeedError: null,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("writes feed error to _feedStore", async () => {
|
|
72
|
+
const error = new Error("feed failed");
|
|
73
|
+
const data = { fallback: true };
|
|
74
|
+
|
|
75
|
+
mockUseFeedLoader.mockReturnValue({
|
|
76
|
+
data,
|
|
77
|
+
loading: false,
|
|
78
|
+
error,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
render(
|
|
82
|
+
<ScreenFeedLoader id="test" feedData={{ source: "url", mapping: {} }}>
|
|
83
|
+
<Text>child</Text>
|
|
84
|
+
</ScreenFeedLoader>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
await waitFor(() => {
|
|
88
|
+
expect(mockSetState).toHaveBeenCalledWith({
|
|
89
|
+
screenFeed: data,
|
|
90
|
+
screenFeedError: error,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ScreenFeedLoader } from "./ScreenFeedLoader";
|
|
@@ -53,6 +53,9 @@ const mockComponents = { ScreenType1, ScreenType2, PlayerController };
|
|
|
53
53
|
|
|
54
54
|
const mockState = {
|
|
55
55
|
components: mockComponents,
|
|
56
|
+
remoteConfigurations: {
|
|
57
|
+
assets: {},
|
|
58
|
+
},
|
|
56
59
|
plugins: [
|
|
57
60
|
mockScreenType3,
|
|
58
61
|
mockScreenType4,
|
|
@@ -127,6 +130,21 @@ const getWrapper = (screenId, screenType, screenData) => {
|
|
|
127
130
|
);
|
|
128
131
|
};
|
|
129
132
|
|
|
133
|
+
const getWrappedWrapper = (screenId, screenType, screenData) => {
|
|
134
|
+
const ScreenResolver = require("../").ScreenResolver;
|
|
135
|
+
|
|
136
|
+
return renderWithProviders(
|
|
137
|
+
<ScreenResolver
|
|
138
|
+
{...{
|
|
139
|
+
screenId,
|
|
140
|
+
screenType,
|
|
141
|
+
screenData,
|
|
142
|
+
}}
|
|
143
|
+
/>,
|
|
144
|
+
mockState
|
|
145
|
+
);
|
|
146
|
+
};
|
|
147
|
+
|
|
130
148
|
describe("<ScreenResolver />", () => {
|
|
131
149
|
it("renders correctly", () => {
|
|
132
150
|
const wrapper = getWrapper("1234", "screen_type_1", {});
|
|
@@ -134,6 +152,12 @@ describe("<ScreenResolver />", () => {
|
|
|
134
152
|
expect(wrapper.getByTestId("screen_type_1")).toBeDefined();
|
|
135
153
|
});
|
|
136
154
|
|
|
155
|
+
it("renders correctly when wrapped with default screen context", () => {
|
|
156
|
+
const wrapper = getWrappedWrapper("1234", "screen_type_1", {});
|
|
157
|
+
|
|
158
|
+
expect(wrapper.getByTestId("screen_type_1")).toBeDefined();
|
|
159
|
+
});
|
|
160
|
+
|
|
137
161
|
it("picks screen from plugins if it exists", () => {
|
|
138
162
|
const wrapper = getWrapper("A1234", "screen_type_3", {});
|
|
139
163
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { toPascalCase } from "@applicaster/zapp-react-native-utils/stringUtils";
|
|
4
|
+
import {
|
|
5
|
+
useAppSelector,
|
|
6
|
+
selectComponents,
|
|
7
|
+
} from "@applicaster/zapp-react-native-redux";
|
|
8
|
+
|
|
9
|
+
export const useGetComponent = (screenType) => {
|
|
10
|
+
const components = useAppSelector(selectComponents);
|
|
11
|
+
|
|
12
|
+
return React.useMemo(() => {
|
|
13
|
+
return components[toPascalCase(screenType)];
|
|
14
|
+
}, [components, screenType]);
|
|
15
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { path, prop } from "ramda";
|
|
3
|
+
import {
|
|
4
|
+
findPluginByType,
|
|
5
|
+
findPluginByIdentifier,
|
|
6
|
+
} from "@applicaster/zapp-react-native-utils/pluginUtils";
|
|
7
|
+
import { HandlePlayable } from "../../HandlePlayable";
|
|
8
|
+
import { HookRenderer } from "../../HookRenderer";
|
|
9
|
+
import { LinkHandler } from "../../LinkHandler";
|
|
10
|
+
import { Favorites } from "../../Favorites";
|
|
11
|
+
import { usePlugins } from "@applicaster/zapp-react-native-redux/hooks";
|
|
12
|
+
import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
13
|
+
|
|
14
|
+
import { useCallbackActions } from "@applicaster/zapp-react-native-utils/zappFrameworkUtils/HookCallback/useCallbackActions";
|
|
15
|
+
import { useGetComponent } from "./useGetComponent";
|
|
16
|
+
import { getScreenTypeProps } from "../utils";
|
|
17
|
+
|
|
18
|
+
export enum PresentationType {
|
|
19
|
+
Standalone = "Standalone",
|
|
20
|
+
Hook = "Hook",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const screenTypeComponents = {
|
|
24
|
+
favorites: Favorites,
|
|
25
|
+
link: LinkHandler,
|
|
26
|
+
playable: HandlePlayable,
|
|
27
|
+
hooks: HookRenderer,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const useScreenComponentResolver = (screenType, props) => {
|
|
31
|
+
const plugins = usePlugins();
|
|
32
|
+
const { hookPlugin } = props.screenData || {};
|
|
33
|
+
const component = useGetComponent(screenType);
|
|
34
|
+
|
|
35
|
+
const screenAction = useCallbackActions(
|
|
36
|
+
hookPlugin || props.screenData,
|
|
37
|
+
props.screenData.callback
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const {
|
|
41
|
+
videoModalState: { mode },
|
|
42
|
+
} = useNavigation();
|
|
43
|
+
|
|
44
|
+
const componentProps = {
|
|
45
|
+
...props,
|
|
46
|
+
mode,
|
|
47
|
+
screenAction,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const ScreenTypeComponent = screenTypeComponents?.[screenType];
|
|
51
|
+
|
|
52
|
+
if (ScreenTypeComponent) {
|
|
53
|
+
return (
|
|
54
|
+
<ScreenTypeComponent
|
|
55
|
+
{...getScreenTypeProps(screenType, componentProps)}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const ScreenPlugin =
|
|
61
|
+
findPluginByType(screenType, plugins, { skipWarning: true }) ||
|
|
62
|
+
findPluginByIdentifier(screenType, plugins) ||
|
|
63
|
+
findPluginByIdentifier(hookPlugin && hookPlugin.identifier, plugins) ||
|
|
64
|
+
component;
|
|
65
|
+
|
|
66
|
+
const ScreenComponent =
|
|
67
|
+
path(["module", "Component"], ScreenPlugin) ||
|
|
68
|
+
prop("module", ScreenPlugin) ||
|
|
69
|
+
prop("Component", ScreenPlugin) ||
|
|
70
|
+
ScreenPlugin;
|
|
71
|
+
|
|
72
|
+
const configuration =
|
|
73
|
+
prop("configuration", ScreenPlugin) ||
|
|
74
|
+
prop("__plugin_configuration", ScreenComponent);
|
|
75
|
+
|
|
76
|
+
if (!ScreenComponent) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<ScreenComponent
|
|
82
|
+
{...props}
|
|
83
|
+
callback={props.resultCallback || screenAction}
|
|
84
|
+
screenId={props.screenId}
|
|
85
|
+
screenData={props.screenData}
|
|
86
|
+
presentationType={PresentationType.Standalone}
|
|
87
|
+
configuration={configuration}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
findPluginByType,
|
|
5
|
-
findPluginByIdentifier,
|
|
6
|
-
} from "@applicaster/zapp-react-native-utils/pluginUtils";
|
|
7
|
-
import { HandlePlayable } from "../HandlePlayable";
|
|
8
|
-
import { toPascalCase } from "@applicaster/zapp-react-native-utils/stringUtils";
|
|
9
|
-
import { HookRenderer } from "../HookRenderer";
|
|
10
|
-
import { LinkHandler } from "../LinkHandler";
|
|
11
|
-
import { Favorites } from "../Favorites";
|
|
12
|
-
import { ZappPipesScreenContext } from "../../Contexts";
|
|
2
|
+
|
|
13
3
|
import { componentsLogger } from "../../Helpers/logger";
|
|
14
|
-
import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
|
|
15
|
-
import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
16
4
|
import { useScreenAnalytics } from "@applicaster/zapp-react-native-utils/analyticsUtils/helpers/hooks";
|
|
17
5
|
|
|
6
|
+
import { useScreenComponentResolver } from "./hooks/useScreenComponentResolver";
|
|
7
|
+
import { withDefaultScreenContext } from "./withDefaultScreenContext";
|
|
8
|
+
|
|
9
|
+
export enum PresentationType {
|
|
10
|
+
Standalone = "Standalone",
|
|
11
|
+
Hook = "Hook",
|
|
12
|
+
}
|
|
13
|
+
|
|
18
14
|
const logger = componentsLogger.addSubsystem("ScreenResolver");
|
|
19
15
|
|
|
20
16
|
type Props = {
|
|
@@ -34,85 +30,13 @@ type Props = {
|
|
|
34
30
|
};
|
|
35
31
|
|
|
36
32
|
export function ScreenResolverComponent(props: Props) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const { screenType, screenId, screenData, groupId } = props;
|
|
40
|
-
|
|
41
|
-
const { hookPlugin } = screenData || {};
|
|
42
|
-
|
|
43
|
-
const { components, plugins, rivers } = usePickFromState([
|
|
44
|
-
"components",
|
|
45
|
-
"plugins",
|
|
46
|
-
"rivers",
|
|
47
|
-
]);
|
|
48
|
-
|
|
49
|
-
const {
|
|
50
|
-
videoModalState: { mode },
|
|
51
|
-
} = useNavigation();
|
|
52
|
-
|
|
53
|
-
const [, setScreenContext] = ZappPipesScreenContext.useZappPipesContext();
|
|
54
|
-
|
|
55
|
-
React.useEffect(() => {
|
|
56
|
-
setScreenContext(rivers[screenId]);
|
|
57
|
-
}, [screenId]);
|
|
33
|
+
const { screenType } = props;
|
|
34
|
+
const component = useScreenComponentResolver(screenType, props);
|
|
58
35
|
|
|
59
|
-
|
|
60
|
-
findPluginByType(screenType, plugins, { skipWarning: true }) ||
|
|
61
|
-
findPluginByIdentifier(screenType, plugins) ||
|
|
62
|
-
findPluginByIdentifier(hookPlugin && hookPlugin.identifier, plugins) ||
|
|
63
|
-
components[toPascalCase(screenType)];
|
|
64
|
-
|
|
65
|
-
if (screenType === "favorites") {
|
|
66
|
-
return <Favorites screenData={screenData} />;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (screenType === "link") {
|
|
70
|
-
return <LinkHandler screenData={screenData} />;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (screenType === "playable") {
|
|
74
|
-
return (
|
|
75
|
-
// @ts-ignore
|
|
76
|
-
<HandlePlayable
|
|
77
|
-
item={screenData}
|
|
78
|
-
mode={mode === "PIP" ? "PIP" : "FULLSCREEN"}
|
|
79
|
-
isModal={false}
|
|
80
|
-
groupId={groupId}
|
|
81
|
-
/>
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (hookPlugin || screenType === "hooks") {
|
|
86
|
-
return (
|
|
87
|
-
screenData && (
|
|
88
|
-
<HookRenderer
|
|
89
|
-
screenData={screenData}
|
|
90
|
-
focused={props.focused}
|
|
91
|
-
parentFocus={props.parentFocus as ParentFocus}
|
|
92
|
-
/>
|
|
93
|
-
)
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const ScreenComponent =
|
|
98
|
-
path(["module", "Component"], ScreenPlugin) ||
|
|
99
|
-
prop("module", ScreenPlugin) ||
|
|
100
|
-
prop("Component", ScreenPlugin) ||
|
|
101
|
-
ScreenPlugin;
|
|
102
|
-
|
|
103
|
-
const configuration =
|
|
104
|
-
prop("configuration", ScreenPlugin) ||
|
|
105
|
-
prop("__plugin_configuration", ScreenComponent);
|
|
36
|
+
useScreenAnalytics(props);
|
|
106
37
|
|
|
107
|
-
if (
|
|
108
|
-
return
|
|
109
|
-
<ScreenComponent
|
|
110
|
-
{...props}
|
|
111
|
-
screenId={screenId}
|
|
112
|
-
screenData={screenData}
|
|
113
|
-
configuration={configuration}
|
|
114
|
-
/>
|
|
115
|
-
);
|
|
38
|
+
if (component) {
|
|
39
|
+
return component;
|
|
116
40
|
}
|
|
117
41
|
|
|
118
42
|
logger.warning({
|
|
@@ -123,6 +47,4 @@ export function ScreenResolverComponent(props: Props) {
|
|
|
123
47
|
return null;
|
|
124
48
|
}
|
|
125
49
|
|
|
126
|
-
export const ScreenResolver =
|
|
127
|
-
ScreenResolverComponent
|
|
128
|
-
);
|
|
50
|
+
export const ScreenResolver = withDefaultScreenContext(ScreenResolverComponent);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { getScreenTypeProps } from "../getScreenTypeProps";
|
|
2
|
+
|
|
3
|
+
const baseProps = {
|
|
4
|
+
screenData: { id: "entry-1" },
|
|
5
|
+
mode: "PIP",
|
|
6
|
+
screenId: "screen-1",
|
|
7
|
+
groupId: "group-1",
|
|
8
|
+
focused: true,
|
|
9
|
+
parentFocus: { nextFocusDown: { current: null } },
|
|
10
|
+
screenAction: jest.fn(),
|
|
11
|
+
resultCallback: null,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
describe("getScreenTypeProps", () => {
|
|
15
|
+
it("returns props for favorites/link", () => {
|
|
16
|
+
expect(getScreenTypeProps("favorites", baseProps)).toEqual({
|
|
17
|
+
screenData: baseProps.screenData,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
expect(getScreenTypeProps("link", baseProps)).toEqual({
|
|
21
|
+
screenData: baseProps.screenData,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns props for playable", () => {
|
|
26
|
+
expect(getScreenTypeProps("playable", baseProps)).toEqual({
|
|
27
|
+
item: baseProps.screenData,
|
|
28
|
+
mode: "PIP",
|
|
29
|
+
isModal: false,
|
|
30
|
+
groupId: "group-1",
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("returns props for hooks", () => {
|
|
35
|
+
const result = getScreenTypeProps("hooks", baseProps);
|
|
36
|
+
|
|
37
|
+
expect(result).toMatchObject({
|
|
38
|
+
screenData: baseProps.screenData,
|
|
39
|
+
focused: true,
|
|
40
|
+
parentFocus: baseProps.parentFocus,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(result.callback).toBe(baseProps.screenAction);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const getScreenTypeProps = (
|
|
2
|
+
screenType: "favorites" | "link" | "playable" | "hooks",
|
|
3
|
+
props
|
|
4
|
+
) => {
|
|
5
|
+
const {
|
|
6
|
+
screenData,
|
|
7
|
+
mode,
|
|
8
|
+
screenId,
|
|
9
|
+
groupId,
|
|
10
|
+
focused,
|
|
11
|
+
parentFocus,
|
|
12
|
+
screenAction,
|
|
13
|
+
resultCallback,
|
|
14
|
+
} = props;
|
|
15
|
+
|
|
16
|
+
switch (screenType) {
|
|
17
|
+
case "favorites":
|
|
18
|
+
case "link":
|
|
19
|
+
return {
|
|
20
|
+
screenData,
|
|
21
|
+
};
|
|
22
|
+
case "playable":
|
|
23
|
+
return {
|
|
24
|
+
item: screenData,
|
|
25
|
+
mode: mode === "PIP" ? "PIP" : "FULLSCREEN",
|
|
26
|
+
isModal: false,
|
|
27
|
+
groupId: groupId,
|
|
28
|
+
};
|
|
29
|
+
case "hooks":
|
|
30
|
+
return {
|
|
31
|
+
screenData,
|
|
32
|
+
callback: resultCallback || screenAction,
|
|
33
|
+
focused,
|
|
34
|
+
parentFocus: parentFocus as ParentFocus,
|
|
35
|
+
};
|
|
36
|
+
default:
|
|
37
|
+
return {
|
|
38
|
+
callback: resultCallback || screenAction,
|
|
39
|
+
screenId: screenId,
|
|
40
|
+
screenData,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getScreenTypeProps } from "./getScreenTypeProps";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { useRivers } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
3
|
+
import { ZappPipesScreenContext } from "../../Contexts";
|
|
4
|
+
|
|
5
|
+
export function withDefaultScreenContext(Component: React.ComponentType<any>) {
|
|
6
|
+
return function WithDefaultScreenContext(props: any) {
|
|
7
|
+
const screenId = props.screenId;
|
|
8
|
+
const rivers = useRivers();
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<ZappPipesScreenContext.Provider initialContextValue={rivers[screenId]}>
|
|
12
|
+
<Component {...props} />
|
|
13
|
+
</ZappPipesScreenContext.Provider>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ScreenFeedLoader } from "../ScreenFeedLoader/ScreenFeedLoader";
|
|
3
|
+
|
|
4
|
+
/** Resolves screen-feed for a given screen `id` by using the provided `useFeedData` hook */
|
|
5
|
+
export const ScreenResolverFeedProvider = ({
|
|
6
|
+
id,
|
|
7
|
+
children,
|
|
8
|
+
useFeedData,
|
|
9
|
+
}: {
|
|
10
|
+
id: string;
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
useFeedData: (id: string) => Option<ZappDataSource>;
|
|
13
|
+
}) => {
|
|
14
|
+
const feedData = useFeedData(id);
|
|
15
|
+
|
|
16
|
+
if (feedData?.source) {
|
|
17
|
+
return (
|
|
18
|
+
<ScreenFeedLoader id={id} feedData={feedData}>
|
|
19
|
+
{children}
|
|
20
|
+
</ScreenFeedLoader>
|
|
21
|
+
);
|
|
22
|
+
} else {
|
|
23
|
+
return <>{children}</>;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text } from "react-native";
|
|
3
|
+
import { render } from "@testing-library/react-native";
|
|
4
|
+
import { ScreenResolverFeedProvider } from "../ScreenResolverFeedProvider";
|
|
5
|
+
|
|
6
|
+
jest.mock("../../ScreenFeedLoader/ScreenFeedLoader", () => ({
|
|
7
|
+
ScreenFeedLoader: ({ children }) => {
|
|
8
|
+
const React = require("react");
|
|
9
|
+
const { View } = require("react-native");
|
|
10
|
+
|
|
11
|
+
return <View testID="feed-loader">{children}</View>;
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
describe("ScreenResolverFeedProvider", () => {
|
|
16
|
+
it("renders ScreenFeedLoader when screen feed source exists", () => {
|
|
17
|
+
const useFeedData = jest.fn(() => ({
|
|
18
|
+
source: "https://feed",
|
|
19
|
+
mapping: {},
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const { getByTestId, getByText } = render(
|
|
23
|
+
<ScreenResolverFeedProvider id="screen-1" useFeedData={useFeedData}>
|
|
24
|
+
<Text>content</Text>
|
|
25
|
+
</ScreenResolverFeedProvider>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
expect(getByTestId("feed-loader")).toBeDefined();
|
|
29
|
+
expect(getByText("content")).toBeDefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("renders children directly when screen feed source is missing", () => {
|
|
33
|
+
const useFeedData = jest.fn(() => ({}));
|
|
34
|
+
|
|
35
|
+
const { queryByTestId, getByText } = render(
|
|
36
|
+
<ScreenResolverFeedProvider id="screen-1" useFeedData={useFeedData}>
|
|
37
|
+
<Text>content</Text>
|
|
38
|
+
</ScreenResolverFeedProvider>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect(queryByTestId("feed-loader")).toBeNull();
|
|
42
|
+
expect(getByText("content")).toBeDefined();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ScreenResolverFeedProvider } from "./ScreenResolverFeedProvider";
|
package/Components/index.js
CHANGED
|
@@ -10,7 +10,7 @@ export { ContentScreen } from "./ContentScreen";
|
|
|
10
10
|
|
|
11
11
|
export { TextInputTv } from "./TextInputTv";
|
|
12
12
|
|
|
13
|
-
export { HookRenderer } from "./HookRenderer";
|
|
13
|
+
export { HookRenderer } from "./HookRenderer/HookRenderer";
|
|
14
14
|
|
|
15
15
|
export { Touchable } from "./Touchable";
|
|
16
16
|
|
|
@@ -2,6 +2,32 @@ import { render } from "@testing-library/react-native";
|
|
|
2
2
|
import { ScreenContext, ScreenContextProvider, withScreenContext } from "../";
|
|
3
3
|
import React from "react";
|
|
4
4
|
|
|
5
|
+
jest.mock("@applicaster/zapp-react-native-utils/reactHooks", () => ({
|
|
6
|
+
useCurrentScreenData: jest.fn(() => ({})),
|
|
7
|
+
useNavigation: jest.fn(() => ({
|
|
8
|
+
data: {},
|
|
9
|
+
modalData: null,
|
|
10
|
+
videoModalState: {},
|
|
11
|
+
canGoBack: jest.fn(() => false),
|
|
12
|
+
})),
|
|
13
|
+
useRoute: jest.fn(() => ({ screenData: null })),
|
|
14
|
+
isNavBarVisible: jest.fn(() => true),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
jest.mock(
|
|
18
|
+
"@applicaster/zapp-react-native-ui-components/Contexts/ModalNavigationContext",
|
|
19
|
+
() => ({
|
|
20
|
+
useModalNavigationContext: jest.fn(() => false),
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
jest.mock(
|
|
25
|
+
"@applicaster/zapp-react-native-ui-components/Contexts/NestedNavigationContext",
|
|
26
|
+
() => ({
|
|
27
|
+
useNestedNavigationContext: jest.fn(() => false),
|
|
28
|
+
})
|
|
29
|
+
);
|
|
30
|
+
|
|
5
31
|
describe("ScreenContext", () => {
|
|
6
32
|
describe("ScreneContext context", () => {
|
|
7
33
|
it("should return the context", () => {
|
|
@@ -13,6 +39,37 @@ describe("ScreenContext", () => {
|
|
|
13
39
|
it("should return the provider", () => {
|
|
14
40
|
expect(ScreenContextProvider).toBeDefined();
|
|
15
41
|
});
|
|
42
|
+
|
|
43
|
+
it("recreates _feedStore when pathname changes", () => {
|
|
44
|
+
const contexts = [];
|
|
45
|
+
|
|
46
|
+
const CaptureContext = () => {
|
|
47
|
+
const context = React.useContext(ScreenContext);
|
|
48
|
+
contexts.push(context);
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const { rerender } = render(
|
|
54
|
+
<ScreenContextProvider pathname="/screen-a">
|
|
55
|
+
<CaptureContext />
|
|
56
|
+
</ScreenContextProvider>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const first = contexts[contexts.length - 1];
|
|
60
|
+
first._feedStore.setState({ screenFeed: "screen-a" });
|
|
61
|
+
|
|
62
|
+
rerender(
|
|
63
|
+
<ScreenContextProvider pathname="/screen-b">
|
|
64
|
+
<CaptureContext />
|
|
65
|
+
</ScreenContextProvider>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const second = contexts[contexts.length - 1];
|
|
69
|
+
|
|
70
|
+
expect(second._feedStore).not.toBe(first._feedStore);
|
|
71
|
+
expect(second._feedStore.getState()).toEqual({});
|
|
72
|
+
});
|
|
16
73
|
});
|
|
17
74
|
|
|
18
75
|
describe("withScreenContext", () => {
|
|
@@ -84,9 +84,13 @@ const createStore = () =>
|
|
|
84
84
|
const createScreenComponentsStore = () =>
|
|
85
85
|
create(subscribeWithSelector<Record<string, unknown>>((_) => ({})));
|
|
86
86
|
|
|
87
|
+
const createFeedStore = () =>
|
|
88
|
+
create(subscribeWithSelector<Record<string, unknown>>((_) => ({})));
|
|
89
|
+
|
|
87
90
|
type ScreenContextType = {
|
|
88
91
|
_navBarStore: ReturnType<typeof createStore>;
|
|
89
92
|
_stateStore: ReturnType<typeof createStateStore>;
|
|
93
|
+
_feedStore: ReturnType<typeof createFeedStore>;
|
|
90
94
|
navBar: NavBarState;
|
|
91
95
|
legacyFormatScreenData: LegacyNavigationScreenData | null;
|
|
92
96
|
/**
|
|
@@ -109,6 +113,7 @@ type ScreenContextType = {
|
|
|
109
113
|
export const ScreenContext = createContext<ScreenContextType>({
|
|
110
114
|
_stateStore: createStateStore(),
|
|
111
115
|
_navBarStore: createStore(),
|
|
116
|
+
_feedStore: createFeedStore(),
|
|
112
117
|
navBar: {
|
|
113
118
|
visible: true,
|
|
114
119
|
title: "",
|
|
@@ -150,6 +155,10 @@ export function ScreenContextProvider({
|
|
|
150
155
|
null
|
|
151
156
|
);
|
|
152
157
|
|
|
158
|
+
const screenFeedStoreRef = useRef<null | ReturnType<typeof createFeedStore>>(
|
|
159
|
+
null
|
|
160
|
+
);
|
|
161
|
+
|
|
153
162
|
const getScreenState = useCallback(() => {
|
|
154
163
|
if (screenStateRef.current !== null) {
|
|
155
164
|
return screenStateRef.current;
|
|
@@ -172,6 +181,13 @@ export function ScreenContextProvider({
|
|
|
172
181
|
return navBarState;
|
|
173
182
|
}, []);
|
|
174
183
|
|
|
184
|
+
// Assign feed store to ref to persist it across re-renders, but recreate on pathname change
|
|
185
|
+
const screenFeedStore = useMemo(() => createFeedStore(), [pathname]);
|
|
186
|
+
|
|
187
|
+
if (screenFeedStoreRef.current !== screenFeedStore) {
|
|
188
|
+
screenFeedStoreRef.current = screenFeedStore;
|
|
189
|
+
}
|
|
190
|
+
|
|
175
191
|
// Component state store - recreated when pathname changes (route change).
|
|
176
192
|
// Unlike _navBarStore and _stateStore (cached via refs), this store
|
|
177
193
|
// resets only when pathname changes to provide a clean state for the new route.
|
|
@@ -230,6 +246,7 @@ export function ScreenContextProvider({
|
|
|
230
246
|
() => ({
|
|
231
247
|
_navBarStore: getScreenNavBarState(),
|
|
232
248
|
_stateStore: getScreenState(),
|
|
249
|
+
_feedStore: screenFeedStoreRef.current,
|
|
233
250
|
navBar: navBarState,
|
|
234
251
|
legacyFormatScreenData: routeScreenData,
|
|
235
252
|
_componentStateStore: componentStateStore,
|
|
@@ -5,10 +5,10 @@ import React, {
|
|
|
5
5
|
useMemo,
|
|
6
6
|
useState,
|
|
7
7
|
} from "react";
|
|
8
|
-
import * as R from "ramda";
|
|
9
8
|
|
|
10
|
-
type ProviderProps = {
|
|
9
|
+
type ProviderProps<S> = {
|
|
11
10
|
children: React.ReactChild;
|
|
11
|
+
initialContextValue?: S;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
type ContextType<T> = {
|
|
@@ -27,21 +27,29 @@ export function createZappPipesContext<T, S = T>(
|
|
|
27
27
|
) {
|
|
28
28
|
const Context = createContext<ContextType<S>>(initialContext);
|
|
29
29
|
|
|
30
|
-
const
|
|
30
|
+
const defaultSelector = (c: any) => c;
|
|
31
|
+
const defaultPrepareContext = (n: any) => n;
|
|
32
|
+
const joinArgs = (args: any[]) => args.join("-");
|
|
33
|
+
|
|
34
|
+
const { selector = defaultSelector, prepareContext = defaultPrepareContext } =
|
|
35
|
+
options || {};
|
|
31
36
|
|
|
32
37
|
function useZappPipesContext(...hookArgs: any[]): [T, (T) => void] {
|
|
33
38
|
const { context, setContext } = useContext(Context);
|
|
39
|
+
const joinedArgs = joinArgs(hookArgs);
|
|
34
40
|
|
|
35
41
|
const contextValue = useMemo(
|
|
36
42
|
() => selector(context, ...hookArgs),
|
|
37
|
-
|
|
43
|
+
// eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
|
|
44
|
+
[context, joinedArgs]
|
|
38
45
|
);
|
|
39
46
|
|
|
40
47
|
const contextSetter = useCallback(
|
|
41
48
|
(newContext: T) => {
|
|
42
49
|
setContext(prepareContext(newContext, context, ...hookArgs));
|
|
43
50
|
},
|
|
44
|
-
|
|
51
|
+
// eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
|
|
52
|
+
[context, joinedArgs]
|
|
45
53
|
);
|
|
46
54
|
|
|
47
55
|
return useMemo(
|
|
@@ -50,8 +58,11 @@ export function createZappPipesContext<T, S = T>(
|
|
|
50
58
|
);
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
/** Provider accepts `initialContextValue` prop to set the initial context value */
|
|
62
|
+
function Provider({ children, initialContextValue }: ProviderProps<S>) {
|
|
63
|
+
const [context, setContext] = useState<S>(
|
|
64
|
+
initialContextValue ?? initialContext.context
|
|
65
|
+
);
|
|
55
66
|
|
|
56
67
|
return (
|
|
57
68
|
<Context.Provider value={{ context, setContext }}>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-ui-components",
|
|
3
|
-
"version": "14.0.
|
|
3
|
+
"version": "14.0.22-alpha.4201433163",
|
|
4
4
|
"description": "Applicaster Zapp React Native ui components for the Quick Brick App",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
},
|
|
29
29
|
"homepage": "https://github.com/applicaster/quickbrick#readme",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@applicaster/applicaster-types": "14.0.
|
|
32
|
-
"@applicaster/zapp-react-native-bridge": "14.0.
|
|
33
|
-
"@applicaster/zapp-react-native-redux": "14.0.
|
|
34
|
-
"@applicaster/zapp-react-native-utils": "14.0.
|
|
31
|
+
"@applicaster/applicaster-types": "14.0.22-alpha.4201433163",
|
|
32
|
+
"@applicaster/zapp-react-native-bridge": "14.0.22-alpha.4201433163",
|
|
33
|
+
"@applicaster/zapp-react-native-redux": "14.0.22-alpha.4201433163",
|
|
34
|
+
"@applicaster/zapp-react-native-utils": "14.0.22-alpha.4201433163",
|
|
35
35
|
"fast-json-stable-stringify": "^2.1.0",
|
|
36
36
|
"promise": "^8.3.0",
|
|
37
37
|
"url": "^0.11.0",
|
|
File without changes
|