@applicaster/zapp-react-native-ui-components 15.0.0-alpha.4920595583 → 15.0.0-alpha.5104105031
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/Cell/Cell.tsx +6 -0
- package/Components/GeneralContentScreen/utils/useCurationAPI.ts +7 -7
- package/Components/MasterCell/DefaultComponents/SecondaryImage/Image.tsx +40 -39
- package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/Image.test.tsx +95 -0
- package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/__snapshots__/Image.test.tsx.snap +86 -0
- package/Components/MasterCell/DefaultComponents/SecondaryImage/__tests__/index.test.ts +141 -0
- package/Components/MasterCell/DefaultComponents/SecondaryImage/index.ts +1 -1
- package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/__tests__/getPluginIdentifier.test.ts +126 -1
- package/Components/MasterCell/DefaultComponents/tv/TvActionButtons/utils/index.ts +1 -3
- package/Components/MasterCell/hoc/__tests__/withAsyncRender.test.tsx +219 -0
- package/Components/MasterCell/hoc/withAsyncRender.tsx +9 -7
- package/Components/OfflineHandler/NotificationView/NotificationView.lg.tsx +17 -9
- package/Components/OfflineHandler/NotificationView/NotificationView.samsung.tsx +16 -8
- package/Components/OfflineHandler/NotificationView/utils.ts +34 -0
- package/package.json +5 -5
package/Components/Cell/Cell.tsx
CHANGED
|
@@ -26,11 +26,15 @@ type Props = {
|
|
|
26
26
|
componentAnchorPointY: number;
|
|
27
27
|
headerOffset?: number;
|
|
28
28
|
extraAnchorPointYOffset?: number;
|
|
29
|
+
componentPaddingTop?: number;
|
|
29
30
|
}) => void;
|
|
30
31
|
offsetUpdater: (arg1: string, arg2: number, arg3: number) => number;
|
|
31
32
|
componentId: string;
|
|
32
33
|
component: {
|
|
33
34
|
id: string;
|
|
35
|
+
styles?: {
|
|
36
|
+
component_padding_top?: number;
|
|
37
|
+
};
|
|
34
38
|
};
|
|
35
39
|
selected?: boolean;
|
|
36
40
|
CellRenderer: React.FunctionComponent<any> & {
|
|
@@ -178,6 +182,8 @@ export class CellComponent extends React.Component<Props, State> {
|
|
|
178
182
|
componentAnchorPointY,
|
|
179
183
|
headerOffset,
|
|
180
184
|
extraAnchorPointYOffset,
|
|
185
|
+
componentPaddingTop:
|
|
186
|
+
this.props?.component?.styles?.component_padding_top,
|
|
181
187
|
});
|
|
182
188
|
}
|
|
183
189
|
}
|
|
@@ -9,10 +9,9 @@ import {
|
|
|
9
9
|
import { isEmptyOrNil } from "@applicaster/zapp-react-native-utils/cellUtils";
|
|
10
10
|
import { Categories } from "./logger";
|
|
11
11
|
import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
12
|
-
import {
|
|
12
|
+
import { useScreenContext } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
|
-
ZappPipesEntryContext,
|
|
16
15
|
ZappPipesScreenContext,
|
|
17
16
|
ZappPipesSearchContext,
|
|
18
17
|
} from "@applicaster/zapp-react-native-ui-components/Contexts";
|
|
@@ -136,7 +135,6 @@ export const useCurationAPI = (
|
|
|
136
135
|
[components]
|
|
137
136
|
);
|
|
138
137
|
|
|
139
|
-
const { pathname } = useRoute();
|
|
140
138
|
const [searchContext] = ZappPipesSearchContext.useZappPipesContext();
|
|
141
139
|
const [screenContext] = ZappPipesScreenContext.useZappPipesContext();
|
|
142
140
|
|
|
@@ -146,10 +144,12 @@ export const useCurationAPI = (
|
|
|
146
144
|
screenContextType === TABS_SCREEN_TYPE ||
|
|
147
145
|
screenContextType === QB_TABS_SCREEN_TYPE;
|
|
148
146
|
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
147
|
+
const screenContextData = useScreenContext();
|
|
148
|
+
|
|
149
|
+
const entryContext = ((isNestedScreen && screenContextData?.nested?.entry
|
|
150
|
+
? screenContextData?.nested?.entry
|
|
151
|
+
: (screenContextData?.entry?.payload ?? screenContextData?.entry)) ||
|
|
152
|
+
{}) as ZappEntry;
|
|
153
153
|
|
|
154
154
|
const urlsMap = useMemo<{ [key: string]: string }>(() => {
|
|
155
155
|
const map = {};
|
|
@@ -3,8 +3,8 @@ import { isNil } from "ramda";
|
|
|
3
3
|
import { ImageStyle, View } from "react-native";
|
|
4
4
|
import {
|
|
5
5
|
FIT_POSITION,
|
|
6
|
-
IMAGE_SIZING_FIT,
|
|
7
6
|
IMAGE_SIZING_FILL,
|
|
7
|
+
IMAGE_SIZING_FIT,
|
|
8
8
|
} from "@applicaster/zapp-react-native-utils/manifestUtils/secondaryImage";
|
|
9
9
|
import { QBImage as Image } from "@applicaster/zapp-react-native-ui-components/Components/Image";
|
|
10
10
|
|
|
@@ -25,49 +25,51 @@ interface Props {
|
|
|
25
25
|
fitPosition: typeof FIT_POSITION;
|
|
26
26
|
fixedWidth: number;
|
|
27
27
|
fixedHeight: number;
|
|
28
|
-
onAsyncRender
|
|
28
|
+
onAsyncRender?: () => void;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/** Secondary Image Dynamic does not render until the image is loaded */
|
|
32
|
-
const SecondaryImageDynamic = (
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
const SecondaryImageDynamic = withAsyncRenderHOC(
|
|
33
|
+
(props: Props & { onAsyncRender: () => void }) => {
|
|
34
|
+
const { uri, style, displayMode, imageSizing, fitPosition, onAsyncRender } =
|
|
35
|
+
props;
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
const imageDimension = useGetImageDimensions(
|
|
38
|
+
uri,
|
|
39
|
+
style.width as number,
|
|
40
|
+
isImageSizingFit(imageSizing) ? undefined : (style.height as number)
|
|
41
|
+
);
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
const containerHeight = imageDimension?.height;
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
const containerWidth = style?.width;
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
if (isNil(imageDimension?.aspectRatio)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
51
|
+
return (
|
|
52
|
+
<View style={style} onLayout={onAsyncRender}>
|
|
53
|
+
<Image
|
|
54
|
+
{...props}
|
|
55
|
+
source={{ uri }}
|
|
56
|
+
style={{
|
|
57
|
+
...getStyle({
|
|
58
|
+
imageSizing,
|
|
59
|
+
fitPosition,
|
|
60
|
+
displayMode,
|
|
61
|
+
imageDimension,
|
|
62
|
+
containerHeight,
|
|
63
|
+
containerWidth,
|
|
64
|
+
}),
|
|
65
|
+
borderRadius: style.borderRadius,
|
|
66
|
+
aspectRatio: imageDimension.aspectRatio,
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
69
|
+
</View>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
);
|
|
71
73
|
|
|
72
74
|
/** Secondary Image Fixed does not render the image until the image is loaded, but keep container rendered */
|
|
73
75
|
const SecondaryImageFixed = (props: Props) => {
|
|
@@ -79,7 +81,6 @@ const SecondaryImageFixed = (props: Props) => {
|
|
|
79
81
|
fitPosition,
|
|
80
82
|
fixedHeight,
|
|
81
83
|
fixedWidth,
|
|
82
|
-
onAsyncRender,
|
|
83
84
|
} = props;
|
|
84
85
|
|
|
85
86
|
const imageDimension = useGetImageDimensions(
|
|
@@ -89,7 +90,7 @@ const SecondaryImageFixed = (props: Props) => {
|
|
|
89
90
|
);
|
|
90
91
|
|
|
91
92
|
return (
|
|
92
|
-
<View style={style}
|
|
93
|
+
<View style={style}>
|
|
93
94
|
{isNil(imageDimension?.aspectRatio) ? null : (
|
|
94
95
|
<Image
|
|
95
96
|
{...props}
|
|
@@ -128,4 +129,4 @@ const SecondaryImageComponent = (props: Props) => {
|
|
|
128
129
|
);
|
|
129
130
|
};
|
|
130
131
|
|
|
131
|
-
export const SecondaryImage =
|
|
132
|
+
export const SecondaryImage = SecondaryImageComponent;
|
|
@@ -10,6 +10,10 @@ describe("SecondaryImage - Image", () => {
|
|
|
10
10
|
displayMode="dynamic"
|
|
11
11
|
uri={undefined}
|
|
12
12
|
style={{ width: 100 }}
|
|
13
|
+
imageSizing="fit"
|
|
14
|
+
fitPosition="center"
|
|
15
|
+
fixedWidth={0}
|
|
16
|
+
fixedHeight={0}
|
|
13
17
|
/>
|
|
14
18
|
);
|
|
15
19
|
|
|
@@ -23,6 +27,10 @@ describe("SecondaryImage - Image", () => {
|
|
|
23
27
|
displayMode="dynamic"
|
|
24
28
|
uri="someurl"
|
|
25
29
|
style={{ width: 100 }}
|
|
30
|
+
imageSizing="fit"
|
|
31
|
+
fitPosition="center"
|
|
32
|
+
fixedWidth={0}
|
|
33
|
+
fixedHeight={0}
|
|
26
34
|
/>
|
|
27
35
|
);
|
|
28
36
|
|
|
@@ -36,6 +44,93 @@ describe("SecondaryImage - Image", () => {
|
|
|
36
44
|
uri="someUrl"
|
|
37
45
|
displayMode="dynamic"
|
|
38
46
|
style={{ width: 100, height: 100, borderRadius: 10 }}
|
|
47
|
+
imageSizing="fill"
|
|
48
|
+
fitPosition="center"
|
|
49
|
+
fixedWidth={0}
|
|
50
|
+
fixedHeight={0}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(wrapper.toJSON()).not.toEqual(null);
|
|
55
|
+
expect(wrapper.toJSON()).toMatchSnapshot();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("SecondaryImage should render in fixed mode without image until loaded", async () => {
|
|
59
|
+
const wrapper = await render(
|
|
60
|
+
<SecondaryImage
|
|
61
|
+
displayMode="fixed"
|
|
62
|
+
uri="someurl"
|
|
63
|
+
style={{ width: 100, height: 100, borderRadius: 5 }}
|
|
64
|
+
imageSizing="fit"
|
|
65
|
+
fitPosition="center"
|
|
66
|
+
fixedWidth={100}
|
|
67
|
+
fixedHeight={100}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(wrapper.toJSON()).toMatchSnapshot();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("SecondaryImage should not render in dynamic mode without image", async () => {
|
|
75
|
+
const wrapper = await render(
|
|
76
|
+
<SecondaryImage
|
|
77
|
+
displayMode="dynamic"
|
|
78
|
+
uri={null}
|
|
79
|
+
style={{ width: 100, height: 100 }}
|
|
80
|
+
imageSizing="fit"
|
|
81
|
+
fitPosition="center"
|
|
82
|
+
fixedWidth={0}
|
|
83
|
+
fixedHeight={0}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(wrapper.toJSON()).toEqual(null);
|
|
88
|
+
expect(wrapper.toJSON()).toMatchSnapshot();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("SecondaryImage should render in fixed mode with known dimensions", async () => {
|
|
92
|
+
const wrapper = await render(
|
|
93
|
+
<SecondaryImage
|
|
94
|
+
uri="someUrl"
|
|
95
|
+
displayMode="fixed"
|
|
96
|
+
style={{ width: 100, height: 100, borderRadius: 10 }}
|
|
97
|
+
imageSizing="fill"
|
|
98
|
+
fitPosition="center"
|
|
99
|
+
fixedWidth={100}
|
|
100
|
+
fixedHeight={100}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(wrapper.toJSON()).not.toEqual(null);
|
|
105
|
+
expect(wrapper.toJSON()).toMatchSnapshot();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("SecondaryImage dynamic mode should call onAsyncRender when provided", async () => {
|
|
109
|
+
const wrapper = await render(
|
|
110
|
+
<SecondaryImage
|
|
111
|
+
uri="someUrl"
|
|
112
|
+
displayMode="dynamic"
|
|
113
|
+
style={{ width: 100, height: 100, borderRadius: 10 }}
|
|
114
|
+
imageSizing="fill"
|
|
115
|
+
fitPosition="center"
|
|
116
|
+
fixedWidth={0}
|
|
117
|
+
fixedHeight={0}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect(wrapper.toJSON()).not.toEqual(null);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("SecondaryImage fixed mode should not use async render props", async () => {
|
|
125
|
+
const wrapper = await render(
|
|
126
|
+
<SecondaryImage
|
|
127
|
+
uri="someUrl"
|
|
128
|
+
displayMode="fixed"
|
|
129
|
+
style={{ width: 100, height: 100, borderRadius: 10 }}
|
|
130
|
+
imageSizing="fill"
|
|
131
|
+
fitPosition="center"
|
|
132
|
+
fixedWidth={100}
|
|
133
|
+
fixedHeight={100}
|
|
39
134
|
/>
|
|
40
135
|
);
|
|
41
136
|
|
|
@@ -1,9 +1,45 @@
|
|
|
1
1
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
2
2
|
|
|
3
|
+
exports[`SecondaryImage - Image SecondaryImage fixed mode should not use async render props 1`] = `
|
|
4
|
+
<View
|
|
5
|
+
style={
|
|
6
|
+
{
|
|
7
|
+
"borderRadius": 10,
|
|
8
|
+
"height": 100,
|
|
9
|
+
"width": 100,
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
>
|
|
13
|
+
<Image
|
|
14
|
+
displayMode="fixed"
|
|
15
|
+
fitPosition="center"
|
|
16
|
+
fixedHeight={100}
|
|
17
|
+
fixedWidth={100}
|
|
18
|
+
imageSizing="fill"
|
|
19
|
+
source={
|
|
20
|
+
{
|
|
21
|
+
"uri": "someUrl",
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
style={
|
|
25
|
+
{
|
|
26
|
+
"aspectRatio": 1,
|
|
27
|
+
"borderRadius": 10,
|
|
28
|
+
"height": 100,
|
|
29
|
+
"width": 100,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
uri="someUrl"
|
|
33
|
+
/>
|
|
34
|
+
</View>
|
|
35
|
+
`;
|
|
36
|
+
|
|
3
37
|
exports[`SecondaryImage - Image SecondaryImage should not render if no aspect ratio (dynamic) 1`] = `null`;
|
|
4
38
|
|
|
5
39
|
exports[`SecondaryImage - Image SecondaryImage should not render if no uri 1`] = `null`;
|
|
6
40
|
|
|
41
|
+
exports[`SecondaryImage - Image SecondaryImage should not render in dynamic mode without image 1`] = `null`;
|
|
42
|
+
|
|
7
43
|
exports[`SecondaryImage - Image SecondaryImage should render if known dimensions 1`] = `
|
|
8
44
|
<View
|
|
9
45
|
onLayout={[Function]}
|
|
@@ -17,6 +53,10 @@ exports[`SecondaryImage - Image SecondaryImage should render if known dimensions
|
|
|
17
53
|
>
|
|
18
54
|
<Image
|
|
19
55
|
displayMode="dynamic"
|
|
56
|
+
fitPosition="center"
|
|
57
|
+
fixedHeight={0}
|
|
58
|
+
fixedWidth={0}
|
|
59
|
+
imageSizing="fill"
|
|
20
60
|
onAsyncRender={[Function]}
|
|
21
61
|
source={
|
|
22
62
|
{
|
|
@@ -35,3 +75,49 @@ exports[`SecondaryImage - Image SecondaryImage should render if known dimensions
|
|
|
35
75
|
/>
|
|
36
76
|
</View>
|
|
37
77
|
`;
|
|
78
|
+
|
|
79
|
+
exports[`SecondaryImage - Image SecondaryImage should render in fixed mode with known dimensions 1`] = `
|
|
80
|
+
<View
|
|
81
|
+
style={
|
|
82
|
+
{
|
|
83
|
+
"borderRadius": 10,
|
|
84
|
+
"height": 100,
|
|
85
|
+
"width": 100,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
>
|
|
89
|
+
<Image
|
|
90
|
+
displayMode="fixed"
|
|
91
|
+
fitPosition="center"
|
|
92
|
+
fixedHeight={100}
|
|
93
|
+
fixedWidth={100}
|
|
94
|
+
imageSizing="fill"
|
|
95
|
+
source={
|
|
96
|
+
{
|
|
97
|
+
"uri": "someUrl",
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
style={
|
|
101
|
+
{
|
|
102
|
+
"aspectRatio": 1,
|
|
103
|
+
"borderRadius": 10,
|
|
104
|
+
"height": 100,
|
|
105
|
+
"width": 100,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
uri="someUrl"
|
|
109
|
+
/>
|
|
110
|
+
</View>
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
exports[`SecondaryImage - Image SecondaryImage should render in fixed mode without image until loaded 1`] = `
|
|
114
|
+
<View
|
|
115
|
+
style={
|
|
116
|
+
{
|
|
117
|
+
"borderRadius": 5,
|
|
118
|
+
"height": 100,
|
|
119
|
+
"width": 100,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/>
|
|
123
|
+
`;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { SecondaryImage } from "../index";
|
|
2
|
+
|
|
3
|
+
describe("SecondaryImage - Configuration Builder", () => {
|
|
4
|
+
const mockValue = (config: any) => (key: string, transformer?: Function) => {
|
|
5
|
+
const value = config[key];
|
|
6
|
+
|
|
7
|
+
return transformer ? transformer(value) : value;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
it("should pass false as second argument to image_src_from_media_item", () => {
|
|
11
|
+
const config = {
|
|
12
|
+
secondary_image_switch: true,
|
|
13
|
+
secondary_image_position: "over_image",
|
|
14
|
+
secondary_image_visibility: "always",
|
|
15
|
+
secondary_image_image_key: "custom_image",
|
|
16
|
+
secondary_image_display_mode: "dynamic",
|
|
17
|
+
secondary_image_dynamic_width: 100,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const result = SecondaryImage({
|
|
21
|
+
value: mockValue(config),
|
|
22
|
+
currentPosition: "over_image",
|
|
23
|
+
state: "default",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(result).not.toBeNull();
|
|
27
|
+
expect(result?.elements).toBeDefined();
|
|
28
|
+
expect(result?.elements[0].data).toBeDefined();
|
|
29
|
+
|
|
30
|
+
const imageData = result?.elements[0].data[0];
|
|
31
|
+
expect(imageData.func).toBe("image_src_from_media_item");
|
|
32
|
+
expect(imageData.args).toEqual(["custom_image", false]);
|
|
33
|
+
expect(imageData.propName).toBe("uri");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should return null when secondary image is disabled", () => {
|
|
37
|
+
const config = {
|
|
38
|
+
secondary_image_switch: false,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const result = SecondaryImage({
|
|
42
|
+
value: mockValue(config),
|
|
43
|
+
currentPosition: "over_image",
|
|
44
|
+
state: "default",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(result).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should return null when position does not match currentPosition", () => {
|
|
51
|
+
const config = {
|
|
52
|
+
secondary_image_switch: true,
|
|
53
|
+
secondary_image_position: "above_text_label_1",
|
|
54
|
+
secondary_image_visibility: "always",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const result = SecondaryImage({
|
|
58
|
+
value: mockValue(config),
|
|
59
|
+
currentPosition: "over_image",
|
|
60
|
+
state: "default",
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(result).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should return null when visibility does not match state", () => {
|
|
67
|
+
const config = {
|
|
68
|
+
secondary_image_switch: true,
|
|
69
|
+
secondary_image_position: "over_image",
|
|
70
|
+
secondary_image_visibility: "focused",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const result = SecondaryImage({
|
|
74
|
+
value: mockValue(config),
|
|
75
|
+
currentPosition: "over_image",
|
|
76
|
+
state: "default",
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
expect(result).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should include display mode and image sizing in additionalProps", () => {
|
|
83
|
+
const config = {
|
|
84
|
+
secondary_image_switch: true,
|
|
85
|
+
secondary_image_position: "over_image",
|
|
86
|
+
secondary_image_visibility: "always",
|
|
87
|
+
secondary_image_image_key: "logo",
|
|
88
|
+
secondary_image_display_mode: "fixed",
|
|
89
|
+
secondary_image_image_sizing: "fill",
|
|
90
|
+
secondary_image_fit_position: "center",
|
|
91
|
+
secondary_image_fixed_width: 200,
|
|
92
|
+
secondary_image_fixed_height: 150,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const result = SecondaryImage({
|
|
96
|
+
value: mockValue(config),
|
|
97
|
+
currentPosition: "over_image",
|
|
98
|
+
state: "default",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
expect(result?.elements[0].additionalProps).toMatchObject({
|
|
102
|
+
displayMode: "fixed",
|
|
103
|
+
imageSizing: "fill",
|
|
104
|
+
fitPosition: "center",
|
|
105
|
+
fixedWidth: 200,
|
|
106
|
+
fixedHeight: 150,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should apply correct styles including margins and border radius", () => {
|
|
111
|
+
const config = {
|
|
112
|
+
secondary_image_switch: true,
|
|
113
|
+
secondary_image_position: "over_image",
|
|
114
|
+
secondary_image_visibility: "always",
|
|
115
|
+
secondary_image_display_mode: "fixed",
|
|
116
|
+
secondary_image_fixed_width: 100,
|
|
117
|
+
secondary_image_fixed_height: 100,
|
|
118
|
+
secondary_image_corner_radius: 12,
|
|
119
|
+
secondary_image_margin_top: 10,
|
|
120
|
+
secondary_image_margin_left: 5,
|
|
121
|
+
secondary_image_margin_right: 5,
|
|
122
|
+
secondary_image_margin_bottom: 10,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const result = SecondaryImage({
|
|
126
|
+
value: mockValue(config),
|
|
127
|
+
currentPosition: "over_image",
|
|
128
|
+
state: "default",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(result?.elements[0].style).toMatchObject({
|
|
132
|
+
width: 100,
|
|
133
|
+
height: 100,
|
|
134
|
+
borderRadius: 12,
|
|
135
|
+
marginTop: 10,
|
|
136
|
+
marginLeft: 5,
|
|
137
|
+
marginRight: 5,
|
|
138
|
+
marginBottom: 10,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { getPluginIdentifier } from "..";
|
|
1
|
+
import { getPluginIdentifier, memoizedGetPluginIdentifier } from "..";
|
|
2
2
|
|
|
3
3
|
describe("getPluginIdentifier", () => {
|
|
4
4
|
const prefix = "tv_buttons";
|
|
5
5
|
|
|
6
|
+
beforeAll(() => {
|
|
7
|
+
memoizedGetPluginIdentifier.clear();
|
|
8
|
+
});
|
|
9
|
+
|
|
6
10
|
it("returns the first valid plugin identifier", () => {
|
|
11
|
+
expect.assertions(2);
|
|
12
|
+
|
|
7
13
|
const configuration = {
|
|
8
14
|
tv_buttons_button_1_other: "value",
|
|
9
15
|
tv_buttons_button_1_assign_action:
|
|
@@ -15,10 +21,19 @@ describe("getPluginIdentifier", () => {
|
|
|
15
21
|
const index = 0;
|
|
16
22
|
const result = getPluginIdentifier(configuration, prefix, index);
|
|
17
23
|
|
|
24
|
+
const memoizedResult = memoizedGetPluginIdentifier(
|
|
25
|
+
configuration,
|
|
26
|
+
prefix,
|
|
27
|
+
index
|
|
28
|
+
);
|
|
29
|
+
|
|
18
30
|
expect(result).toEqual(configuration.tv_buttons_button_1_assign_action);
|
|
31
|
+
expect(memoizedResult).toEqual(result);
|
|
19
32
|
});
|
|
20
33
|
|
|
21
34
|
it("returns the second valid plugin identifier", () => {
|
|
35
|
+
expect.assertions(2);
|
|
36
|
+
|
|
22
37
|
const configuration = {
|
|
23
38
|
tv_buttons_button_1_other: "value_1",
|
|
24
39
|
tv_buttons_button_1_assign_action:
|
|
@@ -31,19 +46,37 @@ describe("getPluginIdentifier", () => {
|
|
|
31
46
|
const index = 1;
|
|
32
47
|
const result = getPluginIdentifier(configuration, prefix, index);
|
|
33
48
|
|
|
49
|
+
const memoizedResult = memoizedGetPluginIdentifier(
|
|
50
|
+
configuration,
|
|
51
|
+
prefix,
|
|
52
|
+
index
|
|
53
|
+
);
|
|
54
|
+
|
|
34
55
|
expect(result).toEqual(configuration.tv_buttons_button_2_assign_action);
|
|
56
|
+
expect(memoizedResult).toEqual(result);
|
|
35
57
|
});
|
|
36
58
|
|
|
37
59
|
it("returns undefined when there are no assign_action keys", () => {
|
|
60
|
+
expect.assertions(2);
|
|
61
|
+
|
|
38
62
|
const configuration = {};
|
|
39
63
|
const index = 0;
|
|
40
64
|
|
|
41
65
|
const result = getPluginIdentifier(configuration, prefix, index);
|
|
42
66
|
|
|
67
|
+
const memoizedResult = memoizedGetPluginIdentifier(
|
|
68
|
+
configuration,
|
|
69
|
+
prefix,
|
|
70
|
+
index
|
|
71
|
+
);
|
|
72
|
+
|
|
43
73
|
expect(result).toBeUndefined();
|
|
74
|
+
expect(memoizedResult).toBeUndefined();
|
|
44
75
|
});
|
|
45
76
|
|
|
46
77
|
it("skips undefined values and returns the correct plugin identifier", () => {
|
|
78
|
+
expect.assertions(2);
|
|
79
|
+
|
|
47
80
|
const configuration = {
|
|
48
81
|
tv_buttons_button_1_assign_action: "tv_buttons_button_1_assign_action",
|
|
49
82
|
tv_buttons_button_2_assign_action: undefined,
|
|
@@ -53,10 +86,19 @@ describe("getPluginIdentifier", () => {
|
|
|
53
86
|
const index = 1;
|
|
54
87
|
const result = getPluginIdentifier(configuration, prefix, index);
|
|
55
88
|
|
|
89
|
+
const memoizedResult = memoizedGetPluginIdentifier(
|
|
90
|
+
configuration,
|
|
91
|
+
prefix,
|
|
92
|
+
index
|
|
93
|
+
);
|
|
94
|
+
|
|
56
95
|
expect(result).toEqual(configuration.tv_buttons_button_3_assign_action);
|
|
96
|
+
expect(memoizedResult).toEqual(result);
|
|
57
97
|
});
|
|
58
98
|
|
|
59
99
|
it("handles missing intermediate keys and returns the correct plugin identifier", () => {
|
|
100
|
+
expect.assertions(2);
|
|
101
|
+
|
|
60
102
|
const configuration = {
|
|
61
103
|
tv_buttons_button_1_assign_action: "tv_buttons_button_1_assign_action",
|
|
62
104
|
tv_buttons_button_3_assign_action: "tv_buttons_button_3_assign_action",
|
|
@@ -65,10 +107,19 @@ describe("getPluginIdentifier", () => {
|
|
|
65
107
|
const index = 1;
|
|
66
108
|
const result = getPluginIdentifier(configuration, prefix, index);
|
|
67
109
|
|
|
110
|
+
const memoizedResult = memoizedGetPluginIdentifier(
|
|
111
|
+
configuration,
|
|
112
|
+
prefix,
|
|
113
|
+
index
|
|
114
|
+
);
|
|
115
|
+
|
|
68
116
|
expect(result).toEqual(configuration.tv_buttons_button_3_assign_action);
|
|
117
|
+
expect(memoizedResult).toEqual(result);
|
|
69
118
|
});
|
|
70
119
|
|
|
71
120
|
it("skips empty string values and returns the first non-empty assign_action", () => {
|
|
121
|
+
expect.assertions(2);
|
|
122
|
+
|
|
72
123
|
const configuration = {
|
|
73
124
|
tv_buttons_button_1_assign_action: "",
|
|
74
125
|
tv_buttons_button_2_assign_action: "tv_buttons_button_2_assign_action",
|
|
@@ -77,10 +128,19 @@ describe("getPluginIdentifier", () => {
|
|
|
77
128
|
const index = 0;
|
|
78
129
|
const result = getPluginIdentifier(configuration, prefix, index);
|
|
79
130
|
|
|
131
|
+
const memoizedResult = memoizedGetPluginIdentifier(
|
|
132
|
+
configuration,
|
|
133
|
+
prefix,
|
|
134
|
+
index
|
|
135
|
+
);
|
|
136
|
+
|
|
80
137
|
expect(result).toEqual(configuration.tv_buttons_button_2_assign_action);
|
|
138
|
+
expect(memoizedResult).toEqual(result);
|
|
81
139
|
});
|
|
82
140
|
|
|
83
141
|
it("returns undefined for negative index", () => {
|
|
142
|
+
expect.assertions(2);
|
|
143
|
+
|
|
84
144
|
const configuration = {
|
|
85
145
|
tv_buttons_button_1_assign_action: "a",
|
|
86
146
|
tv_buttons_button_2_assign_action: "b",
|
|
@@ -89,10 +149,19 @@ describe("getPluginIdentifier", () => {
|
|
|
89
149
|
const index = -1;
|
|
90
150
|
const result = getPluginIdentifier(configuration, prefix, index);
|
|
91
151
|
|
|
152
|
+
const memoizedResult = memoizedGetPluginIdentifier(
|
|
153
|
+
configuration,
|
|
154
|
+
prefix,
|
|
155
|
+
index
|
|
156
|
+
);
|
|
157
|
+
|
|
92
158
|
expect(result).toBeUndefined();
|
|
159
|
+
expect(memoizedResult).toBeUndefined();
|
|
93
160
|
});
|
|
94
161
|
|
|
95
162
|
it("returns undefined for out-of-bounds index", () => {
|
|
163
|
+
expect.assertions(2);
|
|
164
|
+
|
|
96
165
|
const configuration = {
|
|
97
166
|
tv_buttons_button_1_assign_action: "a",
|
|
98
167
|
tv_buttons_button_2_assign_action: "b",
|
|
@@ -101,10 +170,19 @@ describe("getPluginIdentifier", () => {
|
|
|
101
170
|
const index = 5;
|
|
102
171
|
const result = getPluginIdentifier(configuration, prefix, index);
|
|
103
172
|
|
|
173
|
+
const memoizedResult = memoizedGetPluginIdentifier(
|
|
174
|
+
configuration,
|
|
175
|
+
prefix,
|
|
176
|
+
index
|
|
177
|
+
);
|
|
178
|
+
|
|
104
179
|
expect(result).toBeUndefined();
|
|
180
|
+
expect(memoizedResult).toBeUndefined();
|
|
105
181
|
});
|
|
106
182
|
|
|
107
183
|
it("ignores keys with wrong prefix or format", () => {
|
|
184
|
+
expect.assertions(2);
|
|
185
|
+
|
|
108
186
|
const configuration = {
|
|
109
187
|
tv_buttons_button_1_assign_action: "a",
|
|
110
188
|
tv_buttons_wrongprefix_2_assign_action: "b",
|
|
@@ -114,10 +192,19 @@ describe("getPluginIdentifier", () => {
|
|
|
114
192
|
const index = 1;
|
|
115
193
|
const result = getPluginIdentifier(configuration, prefix, index);
|
|
116
194
|
|
|
195
|
+
const memoizedResult = memoizedGetPluginIdentifier(
|
|
196
|
+
configuration,
|
|
197
|
+
prefix,
|
|
198
|
+
index
|
|
199
|
+
);
|
|
200
|
+
|
|
117
201
|
expect(result).toBeUndefined();
|
|
202
|
+
expect(memoizedResult).toBeUndefined();
|
|
118
203
|
});
|
|
119
204
|
|
|
120
205
|
it("returns undefined if all assign_actions are null or empty", () => {
|
|
206
|
+
expect.assertions(2);
|
|
207
|
+
|
|
121
208
|
const configuration = {
|
|
122
209
|
tv_buttons_button_1_assign_action: null,
|
|
123
210
|
tv_buttons_button_2_assign_action: undefined,
|
|
@@ -127,10 +214,19 @@ describe("getPluginIdentifier", () => {
|
|
|
127
214
|
const index = 0;
|
|
128
215
|
const result = getPluginIdentifier(configuration, prefix, index);
|
|
129
216
|
|
|
217
|
+
const memoizedResult = memoizedGetPluginIdentifier(
|
|
218
|
+
configuration,
|
|
219
|
+
prefix,
|
|
220
|
+
index
|
|
221
|
+
);
|
|
222
|
+
|
|
130
223
|
expect(result).toBeUndefined();
|
|
224
|
+
expect(memoizedResult).toBeUndefined();
|
|
131
225
|
});
|
|
132
226
|
|
|
133
227
|
it("handles non-string values correctly", () => {
|
|
228
|
+
expect.assertions(6);
|
|
229
|
+
|
|
134
230
|
const configuration = {
|
|
135
231
|
tv_buttons_button_1_assign_action: 123,
|
|
136
232
|
tv_buttons_button_2_assign_action: true,
|
|
@@ -143,10 +239,21 @@ describe("getPluginIdentifier", () => {
|
|
|
143
239
|
expect(getPluginIdentifier(configuration, prefix, 2)).toEqual({
|
|
144
240
|
nested: "value",
|
|
145
241
|
});
|
|
242
|
+
|
|
243
|
+
expect(memoizedGetPluginIdentifier(configuration, prefix, 0)).toEqual(123);
|
|
244
|
+
expect(memoizedGetPluginIdentifier(configuration, prefix, 1)).toEqual(true);
|
|
245
|
+
|
|
246
|
+
expect(memoizedGetPluginIdentifier(configuration, prefix, 2)).toEqual({
|
|
247
|
+
nested: "value",
|
|
248
|
+
});
|
|
146
249
|
});
|
|
147
250
|
});
|
|
148
251
|
|
|
149
252
|
describe("getPluginIdentifier - when configuration has duplicate values", () => {
|
|
253
|
+
beforeAll(() => {
|
|
254
|
+
memoizedGetPluginIdentifier.clear();
|
|
255
|
+
});
|
|
256
|
+
|
|
150
257
|
const prefix = "tv_buttons";
|
|
151
258
|
|
|
152
259
|
const configuration = {
|
|
@@ -155,16 +262,34 @@ describe("getPluginIdentifier - when configuration has duplicate values", () =>
|
|
|
155
262
|
};
|
|
156
263
|
|
|
157
264
|
it("returns the first plugin identifier when values are identical", () => {
|
|
265
|
+
expect.assertions(2);
|
|
266
|
+
|
|
158
267
|
const index = 0;
|
|
159
268
|
const result = getPluginIdentifier(configuration, prefix, index);
|
|
160
269
|
|
|
270
|
+
const memoizedResult = memoizedGetPluginIdentifier(
|
|
271
|
+
configuration,
|
|
272
|
+
prefix,
|
|
273
|
+
index
|
|
274
|
+
);
|
|
275
|
+
|
|
161
276
|
expect(result).toEqual(configuration.tv_buttons_button_1_assign_action);
|
|
277
|
+
expect(memoizedResult).toEqual(result);
|
|
162
278
|
});
|
|
163
279
|
|
|
164
280
|
it("returns the second plugin identifier when values are identical", () => {
|
|
281
|
+
expect.assertions(2);
|
|
282
|
+
|
|
165
283
|
const index = 1;
|
|
166
284
|
const result = getPluginIdentifier(configuration, prefix, index);
|
|
167
285
|
|
|
286
|
+
const memoizedResult = memoizedGetPluginIdentifier(
|
|
287
|
+
configuration,
|
|
288
|
+
prefix,
|
|
289
|
+
index
|
|
290
|
+
);
|
|
291
|
+
|
|
168
292
|
expect(result).toEqual(configuration.tv_buttons_button_2_assign_action);
|
|
293
|
+
expect(memoizedResult).toEqual(result);
|
|
169
294
|
});
|
|
170
295
|
});
|
|
@@ -40,9 +40,7 @@ export const getPluginIdentifier = (
|
|
|
40
40
|
return undefined;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
export const memoizedGetPluginIdentifier = memoizee(getPluginIdentifier
|
|
44
|
-
primitive: true,
|
|
45
|
-
});
|
|
43
|
+
export const memoizedGetPluginIdentifier = memoizee(getPluginIdentifier);
|
|
46
44
|
|
|
47
45
|
type Label = {
|
|
48
46
|
name: string;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "@testing-library/react-native";
|
|
3
|
+
import { Text, View } from "react-native";
|
|
4
|
+
import { withAsyncRenderHOC } from "../withAsyncRender";
|
|
5
|
+
|
|
6
|
+
describe("withAsyncRenderHOC", () => {
|
|
7
|
+
// Test component that requires onAsyncRender
|
|
8
|
+
const TestComponent = ({
|
|
9
|
+
onAsyncRender,
|
|
10
|
+
text,
|
|
11
|
+
}: {
|
|
12
|
+
onAsyncRender: () => void;
|
|
13
|
+
text?: string;
|
|
14
|
+
}) => {
|
|
15
|
+
React.useEffect(() => {
|
|
16
|
+
onAsyncRender();
|
|
17
|
+
}, [onAsyncRender]);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<View>
|
|
21
|
+
<Text>{text || "Test"}</Text>
|
|
22
|
+
</View>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
it("should wrap component and provide onAsyncRender callback", () => {
|
|
27
|
+
const WrappedComponent = withAsyncRenderHOC(TestComponent);
|
|
28
|
+
|
|
29
|
+
const wrapper = render(<WrappedComponent text="Hello" />);
|
|
30
|
+
|
|
31
|
+
expect(wrapper.toJSON()).not.toBeNull();
|
|
32
|
+
expect(wrapper.getByText("Hello")).toBeDefined();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should call emitAsyncElementRegistrate on mount", () => {
|
|
36
|
+
const mockRegistrate = jest.fn();
|
|
37
|
+
const WrappedComponent = withAsyncRenderHOC(TestComponent);
|
|
38
|
+
|
|
39
|
+
render(<WrappedComponent emitAsyncElementRegistrate={mockRegistrate} />);
|
|
40
|
+
|
|
41
|
+
expect(mockRegistrate).toHaveBeenCalledTimes(1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should call emitAsyncElementLayout when onAsyncRender is called", () => {
|
|
45
|
+
const mockLayout = jest.fn();
|
|
46
|
+
|
|
47
|
+
const ComponentWithAsyncCall = ({
|
|
48
|
+
onAsyncRender,
|
|
49
|
+
}: {
|
|
50
|
+
onAsyncRender: () => void;
|
|
51
|
+
}) => {
|
|
52
|
+
return (
|
|
53
|
+
<View onLayout={onAsyncRender}>
|
|
54
|
+
<Text>Async Component</Text>
|
|
55
|
+
</View>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const WrappedComponent = withAsyncRenderHOC(ComponentWithAsyncCall);
|
|
60
|
+
|
|
61
|
+
const wrapper = render(
|
|
62
|
+
<WrappedComponent emitAsyncElementLayout={mockLayout} />
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Trigger onLayout
|
|
66
|
+
const view = wrapper.UNSAFE_getByType(View);
|
|
67
|
+
view.props.onLayout();
|
|
68
|
+
|
|
69
|
+
expect(mockLayout).toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should return unsubscribe function from emitAsyncElementRegistrate", () => {
|
|
73
|
+
const mockUnsubscribe = jest.fn();
|
|
74
|
+
const mockRegistrate = jest.fn(() => mockUnsubscribe);
|
|
75
|
+
const WrappedComponent = withAsyncRenderHOC(TestComponent);
|
|
76
|
+
|
|
77
|
+
const { unmount } = render(
|
|
78
|
+
<WrappedComponent emitAsyncElementRegistrate={mockRegistrate} />
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
expect(mockRegistrate).toHaveBeenCalledTimes(1);
|
|
82
|
+
|
|
83
|
+
// Unmount should trigger cleanup
|
|
84
|
+
unmount();
|
|
85
|
+
|
|
86
|
+
expect(mockUnsubscribe).toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should work without emitAsyncElementRegistrate prop", () => {
|
|
90
|
+
const WrappedComponent = withAsyncRenderHOC(TestComponent);
|
|
91
|
+
|
|
92
|
+
const wrapper = render(<WrappedComponent text="No Registration" />);
|
|
93
|
+
|
|
94
|
+
expect(wrapper.toJSON()).not.toBeNull();
|
|
95
|
+
expect(wrapper.getByText("No Registration")).toBeDefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should work without emitAsyncElementLayout prop", () => {
|
|
99
|
+
const ComponentWithLayout = ({
|
|
100
|
+
onAsyncRender,
|
|
101
|
+
}: {
|
|
102
|
+
onAsyncRender: () => void;
|
|
103
|
+
}) => {
|
|
104
|
+
return (
|
|
105
|
+
<View onLayout={onAsyncRender}>
|
|
106
|
+
<Text>Test Layout</Text>
|
|
107
|
+
</View>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const WrappedComponent = withAsyncRenderHOC(ComponentWithLayout);
|
|
112
|
+
|
|
113
|
+
const wrapper = render(<WrappedComponent />);
|
|
114
|
+
|
|
115
|
+
// Should not throw when calling onLayout without emitAsyncElementLayout
|
|
116
|
+
const view = wrapper.UNSAFE_getByType(View);
|
|
117
|
+
expect(() => view.props.onLayout()).not.toThrow();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should pass through all other props to wrapped component", () => {
|
|
121
|
+
const ComponentWithProps = ({
|
|
122
|
+
customProp,
|
|
123
|
+
anotherProp,
|
|
124
|
+
}: {
|
|
125
|
+
onAsyncRender: () => void;
|
|
126
|
+
customProp: string;
|
|
127
|
+
anotherProp: number;
|
|
128
|
+
}) => {
|
|
129
|
+
return (
|
|
130
|
+
<View>
|
|
131
|
+
<Text>{customProp}</Text>
|
|
132
|
+
<Text>{anotherProp}</Text>
|
|
133
|
+
</View>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const WrappedComponent = withAsyncRenderHOC(ComponentWithProps);
|
|
138
|
+
|
|
139
|
+
const wrapper = render(
|
|
140
|
+
<WrappedComponent customProp="custom value" anotherProp={42} />
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(wrapper.getByText("custom value")).toBeDefined();
|
|
144
|
+
expect(wrapper.getByText("42")).toBeDefined();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should call emitAsyncElementLayout multiple times if onAsyncRender is called multiple times", () => {
|
|
148
|
+
const mockLayout = jest.fn();
|
|
149
|
+
|
|
150
|
+
const ComponentWithMultipleCalls = ({
|
|
151
|
+
onAsyncRender,
|
|
152
|
+
}: {
|
|
153
|
+
onAsyncRender: () => void;
|
|
154
|
+
}) => {
|
|
155
|
+
return (
|
|
156
|
+
<View>
|
|
157
|
+
<View onLayout={onAsyncRender} testID="view1">
|
|
158
|
+
<Text>First</Text>
|
|
159
|
+
</View>
|
|
160
|
+
<View onLayout={onAsyncRender} testID="view2">
|
|
161
|
+
<Text>Second</Text>
|
|
162
|
+
</View>
|
|
163
|
+
</View>
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const WrappedComponent = withAsyncRenderHOC(ComponentWithMultipleCalls);
|
|
168
|
+
|
|
169
|
+
const wrapper = render(
|
|
170
|
+
<WrappedComponent emitAsyncElementLayout={mockLayout} />
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Trigger both onLayout callbacks
|
|
174
|
+
const view1 = wrapper.getByTestId("view1");
|
|
175
|
+
const view2 = wrapper.getByTestId("view2");
|
|
176
|
+
|
|
177
|
+
view1.props.onLayout();
|
|
178
|
+
view2.props.onLayout();
|
|
179
|
+
|
|
180
|
+
expect(mockLayout).toHaveBeenCalledTimes(2);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should handle components that use onAsyncRender in effects", () => {
|
|
184
|
+
const mockLayout = jest.fn();
|
|
185
|
+
|
|
186
|
+
const ComponentWithEffect = ({
|
|
187
|
+
onAsyncRender,
|
|
188
|
+
}: {
|
|
189
|
+
onAsyncRender: () => void;
|
|
190
|
+
}) => {
|
|
191
|
+
React.useEffect(() => {
|
|
192
|
+
// Simulate async operation completing
|
|
193
|
+
const timer = setTimeout(() => {
|
|
194
|
+
onAsyncRender();
|
|
195
|
+
}, 0);
|
|
196
|
+
|
|
197
|
+
return () => clearTimeout(timer);
|
|
198
|
+
}, [onAsyncRender]);
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<View>
|
|
202
|
+
<Text>Effect Component</Text>
|
|
203
|
+
</View>
|
|
204
|
+
);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const WrappedComponent = withAsyncRenderHOC(ComponentWithEffect);
|
|
208
|
+
|
|
209
|
+
render(<WrappedComponent emitAsyncElementLayout={mockLayout} />);
|
|
210
|
+
|
|
211
|
+
// The effect should have been triggered
|
|
212
|
+
return new Promise((resolve) => {
|
|
213
|
+
setTimeout(() => {
|
|
214
|
+
expect(mockLayout).toHaveBeenCalled();
|
|
215
|
+
resolve(true);
|
|
216
|
+
}, 10);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
@@ -6,12 +6,12 @@ type withAsyncRenderHOCProps = {
|
|
|
6
6
|
emitAsyncElementLayout?: () => void;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
Component: React.ComponentType<
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
export const withAsyncRenderHOC = <P extends { onAsyncRender: () => void }>(
|
|
10
|
+
Component: React.ComponentType<P>
|
|
11
|
+
): React.FC<Omit<P, "onAsyncRender"> & withAsyncRenderHOCProps> => {
|
|
12
|
+
const WithAsyncRender = (
|
|
13
|
+
props: Omit<P, "onAsyncRender"> & withAsyncRenderHOCProps
|
|
14
|
+
) => {
|
|
15
15
|
const { emitAsyncElementRegistrate = noop, emitAsyncElementLayout = noop } =
|
|
16
16
|
props;
|
|
17
17
|
|
|
@@ -27,7 +27,9 @@ export const withAsyncRenderHOC: withAsyncRenderHOCT = (Component) => {
|
|
|
27
27
|
}
|
|
28
28
|
}, [emitAsyncElementLayout]);
|
|
29
29
|
|
|
30
|
-
return
|
|
30
|
+
return (
|
|
31
|
+
<Component {...(props as unknown as P)} onAsyncRender={onAsyncRender} />
|
|
32
|
+
);
|
|
31
33
|
};
|
|
32
34
|
|
|
33
35
|
return WithAsyncRender;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
|
|
3
|
+
import { useNetworkStatusLocalizations } from "./utils";
|
|
2
4
|
|
|
3
5
|
type Props = {
|
|
4
6
|
children?: React.ReactNode;
|
|
@@ -10,13 +12,6 @@ type Props = {
|
|
|
10
12
|
|
|
11
13
|
const NOTIF_DURATION = 4500;
|
|
12
14
|
|
|
13
|
-
export const ONLINE_MSG = "You are back online";
|
|
14
|
-
|
|
15
|
-
export const OFFLINE_MSG = "No internet connection";
|
|
16
|
-
|
|
17
|
-
const EXTRA_OFFLINE_MSG = "Please check your connection";
|
|
18
|
-
const EXTRA_ONLINE_MSG = "Feel free to continue where you left off";
|
|
19
|
-
|
|
20
15
|
const styles: Record<any, React.CSSProperties> = {
|
|
21
16
|
body: {
|
|
22
17
|
position: "absolute",
|
|
@@ -47,6 +42,9 @@ const styles: Record<any, React.CSSProperties> = {
|
|
|
47
42
|
export const NotificationView = (props: Props) => {
|
|
48
43
|
const { children, hidden, dismiss, previousOnline, online } = props;
|
|
49
44
|
|
|
45
|
+
const theme = useTheme<BaseThemePropertiesTV>();
|
|
46
|
+
const storedLocalizations = useNetworkStatusLocalizations();
|
|
47
|
+
|
|
50
48
|
const [open, setOpen] = React.useState<boolean | null>(null);
|
|
51
49
|
|
|
52
50
|
const timeout = React.useRef<NodeJS.Timeout>();
|
|
@@ -84,8 +82,18 @@ export const NotificationView = (props: Props) => {
|
|
|
84
82
|
}, [hidden, online]);
|
|
85
83
|
|
|
86
84
|
const showOnlineMsg = wentOnline || online;
|
|
87
|
-
|
|
88
|
-
const
|
|
85
|
+
|
|
86
|
+
const MSG = showOnlineMsg
|
|
87
|
+
? (theme?.online_notification_title ??
|
|
88
|
+
storedLocalizations?.online_notification_title)
|
|
89
|
+
: (theme?.offline_notification_title ??
|
|
90
|
+
storedLocalizations?.offline_notification_title);
|
|
91
|
+
|
|
92
|
+
const EXTRA_MSG = showOnlineMsg
|
|
93
|
+
? (theme?.online_notification_subtitle ??
|
|
94
|
+
storedLocalizations?.online_notification_subtitle)
|
|
95
|
+
: (theme?.offline_notification_subtitle ??
|
|
96
|
+
storedLocalizations?.offline_notification_subtitle);
|
|
89
97
|
|
|
90
98
|
return (
|
|
91
99
|
<div onClick={onClose}>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
|
|
3
|
+
import { useNetworkStatusLocalizations } from "./utils";
|
|
2
4
|
|
|
3
5
|
type Props = {
|
|
4
6
|
children?: React.ReactNode;
|
|
@@ -9,12 +11,6 @@ type Props = {
|
|
|
9
11
|
|
|
10
12
|
const DURATION_TO_HIDE_AFTER_BACK_TO_ONLINE = 4500; // ms
|
|
11
13
|
|
|
12
|
-
const ONLINE_TITLE = "You are back online";
|
|
13
|
-
const ONLINE_SUBTITLE = "Feel free to continue where you left off";
|
|
14
|
-
|
|
15
|
-
const OFFLINE_TITLE = "No internet connection";
|
|
16
|
-
const OFFLINE_SUBTITLE = "Please check your connection";
|
|
17
|
-
|
|
18
14
|
const styles: Record<any, React.CSSProperties> = {
|
|
19
15
|
body: {
|
|
20
16
|
position: "absolute",
|
|
@@ -47,6 +43,9 @@ let timer: NodeJS.Timeout;
|
|
|
47
43
|
export const NotificationView = (props: Props) => {
|
|
48
44
|
const { children, dismiss, previousOnline = true, online } = props;
|
|
49
45
|
|
|
46
|
+
const theme = useTheme<BaseThemePropertiesTV>();
|
|
47
|
+
const storedLocalizations = useNetworkStatusLocalizations();
|
|
48
|
+
|
|
50
49
|
const [shown, setShown] = React.useState<boolean>(false);
|
|
51
50
|
|
|
52
51
|
const onClose = () => {
|
|
@@ -79,8 +78,17 @@ export const NotificationView = (props: Props) => {
|
|
|
79
78
|
}
|
|
80
79
|
}, [online, previousOnline]);
|
|
81
80
|
|
|
82
|
-
const title = online
|
|
83
|
-
|
|
81
|
+
const title = online
|
|
82
|
+
? (theme?.online_notification_title ??
|
|
83
|
+
storedLocalizations?.online_notification_title)
|
|
84
|
+
: (theme?.offline_notification_title ??
|
|
85
|
+
storedLocalizations?.offline_notification_title);
|
|
86
|
+
|
|
87
|
+
const subtitle = online
|
|
88
|
+
? (theme?.online_notification_subtitle ??
|
|
89
|
+
storedLocalizations?.online_notification_subtitle)
|
|
90
|
+
: (theme?.offline_notification_subtitle ??
|
|
91
|
+
storedLocalizations?.offline_notification_subtitle);
|
|
84
92
|
|
|
85
93
|
return (
|
|
86
94
|
<div onClick={onClose}>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { sessionStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/SessionStorage";
|
|
3
|
+
|
|
4
|
+
export const NETWORK_STATUS_LOCALIZATIONS_KEY = "network_status_localizations";
|
|
5
|
+
|
|
6
|
+
export const THEME_STORAGE_NAMESPACE = "quick-brick-theme";
|
|
7
|
+
|
|
8
|
+
export function useNetworkStatusLocalizations() {
|
|
9
|
+
const [storedLocalizations, setStoredLocalizations] = React.useState<Record<
|
|
10
|
+
string,
|
|
11
|
+
string
|
|
12
|
+
> | null>(null);
|
|
13
|
+
|
|
14
|
+
React.useEffect(() => {
|
|
15
|
+
async function loadStoredLocalizations() {
|
|
16
|
+
try {
|
|
17
|
+
const stored = await sessionStorage.getItem(
|
|
18
|
+
NETWORK_STATUS_LOCALIZATIONS_KEY,
|
|
19
|
+
THEME_STORAGE_NAMESPACE
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
if (stored) {
|
|
23
|
+
setStoredLocalizations(stored);
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error("Error loading network status localizations", error);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
loadStoredLocalizations();
|
|
31
|
+
}, []);
|
|
32
|
+
|
|
33
|
+
return storedLocalizations;
|
|
34
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-ui-components",
|
|
3
|
-
"version": "15.0.0-alpha.
|
|
3
|
+
"version": "15.0.0-alpha.5104105031",
|
|
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": "15.0.0-alpha.
|
|
32
|
-
"@applicaster/zapp-react-native-bridge": "15.0.0-alpha.
|
|
33
|
-
"@applicaster/zapp-react-native-redux": "15.0.0-alpha.
|
|
34
|
-
"@applicaster/zapp-react-native-utils": "15.0.0-alpha.
|
|
31
|
+
"@applicaster/applicaster-types": "15.0.0-alpha.5104105031",
|
|
32
|
+
"@applicaster/zapp-react-native-bridge": "15.0.0-alpha.5104105031",
|
|
33
|
+
"@applicaster/zapp-react-native-redux": "15.0.0-alpha.5104105031",
|
|
34
|
+
"@applicaster/zapp-react-native-utils": "15.0.0-alpha.5104105031",
|
|
35
35
|
"fast-json-stable-stringify": "^2.1.0",
|
|
36
36
|
"promise": "^8.3.0",
|
|
37
37
|
"url": "^0.11.0",
|