Skip to content

Commit 32fdad4

Browse files
Add unit test environment setup (#12)
* Setup and add tests (Jest, @testing-library/react-native)
1 parent cfcb119 commit 32fdad4

File tree

13 files changed

+6950
-149
lines changed

13 files changed

+6950
-149
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@ yarn-error.log
44

55
### GENERATED ###
66
dist/
7+
8+
### IDE's ###
9+
.idea/

jest.config.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"preset": "react-native",
3+
"transform": {
4+
"^.+\\.js$": "<rootDir>/node_modules/react-native/jest/preprocessor.js",
5+
"^.+\\.tsx?$": "ts-jest"
6+
},
7+
"testRegex": "test/.*\\.test\\.(tsx?)$",
8+
"moduleFileExtensions": ["ts", "tsx", "js"],
9+
"setupFilesAfterEnv": [
10+
"<rootDir>/test/setup.ts"
11+
],
12+
"transformIgnorePatterns": [
13+
"node_modules/(!react-native)",
14+
"node_modules/(!@react-native-community)"
15+
]
16+
}

package.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,26 @@
1414
},
1515
"scripts": {
1616
"compile": "tsc",
17-
"lint": "tslint -c tslint.json \"!(dist)/**/*.ts?(x)\""
17+
"lint": "tslint -c tslint.json \"!(dist)/**/*.ts?(x)\"",
18+
"test": "jest"
1819
},
1920
"dependencies": {
2021
"react-native-responsive-dimensions": "^3.1.1",
2122
"styled-components": "^5.1.1"
2223
},
2324
"devDependencies": {
24-
"@types/react": "^16.9.44",
25+
"@testing-library/jest-native": "^3.3.0",
26+
"@testing-library/react-native": "^7.0.2",
27+
"@types/jest": "^26.0.10",
2528
"@types/react-native": "^0.63.4",
29+
"@types/react-test-renderer": "^16.9.3",
2630
"@types/styled-components": "^5.1.2",
27-
"react-native-svg": "^12.1.0",
31+
"jest": "^26.4.2",
32+
"react": ">=16.8.0",
33+
"react-native": ">=0.50.0",
34+
"react-native-svg": ">=12.1.0",
35+
"react-test-renderer": "^16.13.1",
36+
"ts-jest": "^26.2.0",
2837
"tslint": "^6.1.3",
2938
"tslint-react": "^5.0.0",
3039
"typescript": "^3.9.7",

src/lib/tour-overlay/TourOverlay.component.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,13 @@ export const TourOverlay: React.FC<TourOverlayProps> = ({ color = "black", opaci
124124
transparent={true}
125125
visible={true}
126126
>
127-
<OverlayView>
128-
<Svg height="100%" width="100%" viewBox={`0 0 ${vwDP(100)} ${vhDP(100)}`}>
127+
<OverlayView accessibilityLabel="Tour Overlay View">
128+
<Svg
129+
accessibilityLabel="Svg overlay view"
130+
height="100%"
131+
width="100%"
132+
viewBox={`0 0 ${vwDP(100)} ${vhDP(100)}`}
133+
>
129134
<Defs>
130135
<Mask id="mask" x={0} y={0} height="100%" width="100%">
131136
<Rect height="100%" width="100%" fill="#fff" />
@@ -147,7 +152,11 @@ export const TourOverlay: React.FC<TourOverlayProps> = ({ color = "black", opaci
147152
/>
148153
</Svg>
149154

150-
<TipView style={[tipStyle, { opacity: tipOpacity }]} onLayout={measureTip}>
155+
<TipView
156+
style={[tipStyle, { opacity: tipOpacity }]}
157+
onLayout={measureTip}
158+
accessibilityLabel="Tip Overlay View"
159+
>
151160
{tourStep.render({
152161
current,
153162
isFirst: current === 0,

test/helpers/TestTour.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import * as React from "react";
2+
import { Button, Text, View } from "react-native";
3+
4+
import { Align, AttachStep, Position, SpotlightTourProvider, TourStep, useSpotlightTour } from "../../src";
5+
6+
export const BASE_STEP: TourStep = {
7+
alignTo: Align.SCREEN,
8+
position: Position.BOTTOM,
9+
render: ({ next, previous }) => (
10+
<View accessibilityLabel="Container fake component" >
11+
<Text>Hello, world!</Text>
12+
<Button accessibilityLabel="Next spot button" title="Next spot" onPress={next} />
13+
<Button accessibilityLabel="Previous spot button" title="Previous spot" onPress={previous} />
14+
</View>
15+
)
16+
};
17+
18+
const TestComponent: React.FC = () => {
19+
const tourContext = useSpotlightTour();
20+
21+
const fakeAction = () => undefined;
22+
23+
return (
24+
<View>
25+
<AttachStep index={0}>
26+
<View>Hello, world!</View>
27+
</AttachStep>
28+
<AttachStep index={1}>
29+
<Button onPress={fakeAction} title="Test button" />
30+
</AttachStep>
31+
<Button
32+
accessibilityLabel="Start tour button"
33+
title="start tour"
34+
onPress={tourContext.start}
35+
/>
36+
<Button
37+
accessibilityLabel="Stop tour button"
38+
title="stop tour"
39+
onPress={tourContext.stop}
40+
/>
41+
</View>
42+
);
43+
};
44+
45+
export const TestScreen: React.FC = () => {
46+
const spotStep = BASE_STEP;
47+
const secondSpotStep = { ...BASE_STEP, position: Position.TOP };
48+
const spotSteps = [spotStep, secondSpotStep];
49+
50+
return (
51+
<SpotlightTourProvider steps={spotSteps}>
52+
<TestComponent />
53+
</SpotlightTourProvider>
54+
);
55+
};

test/helpers/helper.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import * as React from "react";
2+
import { Animated } from "react-native";
3+
import { ReactTestInstance } from "react-test-renderer";
4+
5+
interface Rectangle {
6+
x: number;
7+
y: number;
8+
width: number;
9+
height: number;
10+
}
11+
12+
interface Circle {
13+
y: number;
14+
x: number;
15+
r: number;
16+
}
17+
18+
export function checkValidIntersection(rectangle: Rectangle, circle: Circle): boolean {
19+
/**
20+
* The explanation of the formulas used are available on following the document:
21+
* https://docs.google.com/document/d/1rrfTB7NN4r1HItxiPni83TvL-up3OYXt0dgLOsO9Sg0/edit?usp=sharing
22+
*/
23+
24+
/**
25+
* Rectangles centroid formula:
26+
* https://www.engineeringintro.com/mechanics-of-structures/centre-of-gravity/centroid-of-rectangle/
27+
*/
28+
29+
const rectangleCentroid = {
30+
x: rectangle.x + rectangle.width / 2,
31+
y: rectangle.y + rectangle.height / 2
32+
};
33+
34+
const circleDistanceX = Math.abs(circle.x - rectangleCentroid.x);
35+
const circleDistanceY = Math.abs(circle.y - rectangleCentroid.y);
36+
37+
// Distance between two points formula:
38+
// https://www.mathsisfun.com/algebra/distance-2-points.html
39+
const rectangleCircleCentroidXDistance = Math.pow(rectangleCentroid.x - circle.x, 2);
40+
const rectangleCircleCentroidYDistance = Math.pow(rectangleCentroid.y - circle.y, 2);
41+
const circleAndRectangleCentroidDistance = Math.sqrt(
42+
rectangleCircleCentroidYDistance + rectangleCircleCentroidXDistance
43+
);
44+
45+
const isCircleRadiusShorterThanCentroidsDistance = circle.r <= circleAndRectangleCentroidDistance;
46+
47+
// This formula verifies if the distance between the rectangle centroid and circle centroid
48+
// is larger than the circle radius
49+
const isCentroidsDistanceBiggerThanTheCirclesRadiusSum =
50+
circleAndRectangleCentroidDistance >= rectangle.height / 2 + circle.r &&
51+
circleAndRectangleCentroidDistance >= rectangle.width / 2 + circle.r;
52+
53+
const figuresAreOverlaid =
54+
isCircleRadiusShorterThanCentroidsDistance &&
55+
isCentroidsDistanceBiggerThanTheCirclesRadiusSum;
56+
57+
if (figuresAreOverlaid) {
58+
return false;
59+
}
60+
61+
// A formula that explains this implementation can be found on
62+
// https://math.stackexchange.com/a/2916460
63+
const relativeCircleRectangleXDistance = Math.pow(circleDistanceX - rectangle.width / 2, 2);
64+
const relativeCircleRectangleYDistance = Math.pow(circleDistanceY - rectangle.height / 2, 2);
65+
const cornerDistance = Math.sqrt(
66+
relativeCircleRectangleXDistance + relativeCircleRectangleYDistance
67+
);
68+
69+
const squaredCornerDistanceIsSmallerThanSquaredCircleRadius =
70+
Math.pow(cornerDistance, 2) <= Math.pow(circle.r, 2);
71+
const circleRadiusAndVirtualRectangleRadiusRelation =
72+
circle.r / Math.max(rectangle.width / 2, rectangle.height / 2) >= 1;
73+
74+
return (
75+
squaredCornerDistanceIsSmallerThanSquaredCircleRadius
76+
&& circleRadiusAndVirtualRectangleRadiusRelation
77+
);
78+
}
79+
80+
type ChildProps = { [key: string]: any };
81+
82+
function isReactTestInstance(child: ReactTestInstance | string): child is ReactTestInstance {
83+
return typeof child !== "string";
84+
}
85+
86+
function isReactProps<T extends object>(
87+
props: React.PropsWithChildren<T> | null
88+
): props is React.PropsWithChildren<T> {
89+
return typeof props === "object";
90+
}
91+
92+
export function findPropsOnTestInstance(
93+
reactTestInstance: ReactTestInstance,
94+
componentName: string
95+
): React.PropsWithChildren<ChildProps> {
96+
const findInsideChild = (
97+
childReactTestInstance: ReactTestInstance,
98+
depth: number
99+
): (React.PropsWithChildren<ChildProps> | null)[] => {
100+
if (!isReactTestInstance(childReactTestInstance) || depth <= 0) {
101+
return [null];
102+
}
103+
104+
if (childReactTestInstance.type === componentName) {
105+
return [childReactTestInstance.props];
106+
}
107+
108+
const children: Array<ReactTestInstance | string> = childReactTestInstance.children;
109+
110+
return children.map(nestedChild =>
111+
isReactTestInstance(nestedChild)
112+
? findInsideChild(nestedChild, depth - 1)
113+
: null
114+
);
115+
};
116+
117+
const props = findInsideChild(reactTestInstance, 20)
118+
.flat(Infinity)
119+
.filter(item => !!item)[0];
120+
121+
return isReactProps(props) ? props : {};
122+
}
123+
124+
type AnimatedValue = number | Animated.AnimatedValue | { x: number; y: number } | Animated.AnimatedValueXY;
125+
126+
type TimingAnimatedValue = Animated.AnimatedInterpolation | AnimatedValue;
127+
128+
export function isAnimatedTimingInterpolation(value: TimingAnimatedValue): value is Animated.AnimatedInterpolation {
129+
return Animated.AnimatedInterpolation && value instanceof Animated.AnimatedInterpolation;
130+
}
131+
132+
export function isAnimatedValue(value: AnimatedValue): value is Animated.Value {
133+
return value instanceof Animated.Value;
134+
}
135+
136+
export function isAnimatedValueXY(value: Animated.Value | Animated.ValueXY): value is Animated.ValueXY {
137+
return value instanceof Animated.ValueXY;
138+
}
139+
140+
export function isXYValue(value: AnimatedValue): value is { x: number; y: number } & number {
141+
return !(value instanceof Animated.Value)
142+
&& !(value instanceof Animated.ValueXY)
143+
&& (typeof value === "number" || (typeof value !== "number" && !!(value?.x && value?.y)));
144+
}
145+
146+
export function isNumberValue(value: AnimatedValue): value is number {
147+
return typeof value === "number";
148+
}

test/helpers/measures.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export interface MeasureOnSuccessCallbackParams {
2+
x: number;
3+
y: number;
4+
width: number;
5+
height: number;
6+
}
7+
8+
export const viewMockMeasureData: MeasureOnSuccessCallbackParams = {
9+
height: 400,
10+
width: 200,
11+
x: 1,
12+
y: 1
13+
};
14+
15+
export const buttonMockMeasureData: MeasureOnSuccessCallbackParams = {
16+
height: 50,
17+
width: 100,
18+
x: 10,
19+
y: 10
20+
};

test/helpers/native-mocks.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import * as React from "react";
2+
import {
3+
Animated,
4+
MeasureInWindowOnSuccessCallback,
5+
NativeMethods
6+
} from "react-native";
7+
8+
import { MeasureOnSuccessCallbackParams } from "./measures";
9+
10+
export function mockNativeComponent(modulePath: string, mockMethods: NativeMethods) {
11+
const OriginalComponent = jest.requireActual(modulePath);
12+
const SuperClass = typeof OriginalComponent === "function"
13+
? OriginalComponent
14+
: React.Component;
15+
16+
const Component = class extends SuperClass {
17+
static displayName: string = "Component";
18+
19+
render() {
20+
const name: string =
21+
OriginalComponent.displayName ||
22+
OriginalComponent.name ||
23+
(OriginalComponent.render
24+
? OriginalComponent.render.displayName ||
25+
OriginalComponent.render.name
26+
: "Unknown");
27+
28+
const props = Object.assign({}, OriginalComponent.defaultProps);
29+
30+
if (this.props) {
31+
Object.keys(this.props).forEach(prop => {
32+
if (this.props[prop] !== undefined) {
33+
props[prop] = this.props[prop];
34+
}
35+
});
36+
}
37+
38+
return React.createElement(name.replace(/^(RCT|RK)/, ""), props, this.props.children);
39+
}
40+
};
41+
42+
Object.keys(OriginalComponent).forEach(classStatic => {
43+
Component[classStatic] = OriginalComponent[classStatic];
44+
});
45+
46+
Object.assign(Component.prototype, mockMethods);
47+
48+
return Component;
49+
}
50+
51+
export const emptyNativeMethods: NativeMethods = {
52+
blur: jest.fn(),
53+
focus: jest.fn(),
54+
measure: jest.fn(),
55+
measureInWindow: jest.fn(),
56+
measureLayout: jest.fn(),
57+
refs: {},
58+
setNativeProps: jest.fn()
59+
};
60+
61+
export function createMeasureMethod(
62+
mockMeasureData: MeasureOnSuccessCallbackParams
63+
): (callback: MeasureInWindowOnSuccessCallback) => void {
64+
return callback => {
65+
const { x, y, width, height } = mockMeasureData;
66+
callback(x, y, width, height);
67+
};
68+
}
69+
70+
export const emptyAnimationMethods: Animated.CompositeAnimation = {
71+
reset: () => undefined,
72+
start: () => undefined,
73+
stop: () => undefined
74+
};

0 commit comments

Comments
 (0)