@applicaster/zapp-react-native-ui-components 14.0.7 → 14.0.8
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/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/hoc/__tests__/withAsyncRender.test.tsx +219 -0
- package/Components/MasterCell/hoc/withAsyncRender.tsx +9 -7
- package/package.json +5 -5
|
@@ -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
|
+
});
|
|
@@ -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;
|
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.8",
|
|
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.8",
|
|
32
|
+
"@applicaster/zapp-react-native-bridge": "14.0.8",
|
|
33
|
+
"@applicaster/zapp-react-native-redux": "14.0.8",
|
|
34
|
+
"@applicaster/zapp-react-native-utils": "14.0.8",
|
|
35
35
|
"fast-json-stable-stringify": "^2.1.0",
|
|
36
36
|
"promise": "^8.3.0",
|
|
37
37
|
"url": "^0.11.0",
|