Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): Allow to change native methods behavior #14

Merged
merged 1 commit into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"@babel/register": "^7.23.7",
"@react-native/babel-preset": "^0.73.21",
"babel-plugin-module-resolver": "^5.0.0",
"dot-prop-immutable": "^2.1.1"
"dot-prop-immutable": "^2.1.1",
"ts-pattern": "^5.0.8"
},
"devDependencies": {
"@assertive-ts/core": "^2.1.0",
Expand Down
2 changes: 1 addition & 1 deletion register.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const start = Date.now();
require("./dist/main");
require("./dist/register");

const end = Date.now();
const diff = (end - start) / 1000;
Expand Down
8 changes: 7 additions & 1 deletion src/helpers/mockComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@ import {
ReactNode,
createElement,
} from "react";
import type { NativeMethods } from "react-native";

import type { ScrollViewMethods } from "../lib/Components/ScrollView";
import type { TextInputMethods } from "../lib/Components/TextInput";

export type AllNativeMethods = NativeMethods | ScrollViewMethods | TextInputMethods;

export function mockComponent<P, C extends ComponentClass<PropsWithChildren<P>>>(
RealComponent: C,
instanceMethods?: object | null,
instanceMethods?: AllNativeMethods,
): C {
const SuperClass: ComponentClass<PropsWithChildren<P>> = typeof RealComponent === "function"
? RealComponent
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { NativeMethods } from "react-native";

import { noop } from "./commons";

export const MockNativeMethods = {
export const nativeMethodsMock: NativeMethods = {
blur: noop,
focus: noop,
measure: noop,
measureInWindow: noop,
measureLayout: noop,
refs: { },
setNativeProps: noop,
};
2 changes: 1 addition & 1 deletion src/lib/Components/ActivityIndicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ import { ActivityIndicator } from "react-native";

import { mockComponent } from "../../helpers/mockComponent";

export const ActivityIndicatorMock = mockComponent(ActivityIndicator, null);
export const ActivityIndicatorMock = mockComponent(ActivityIndicator);
23 changes: 16 additions & 7 deletions src/lib/Components/Image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,22 @@ import { Image } from "react-native";
import { noop } from "../../helpers/commons";
import { mockComponent } from "../../helpers/mockComponent";

const Mock = mockComponent(Image as ComponentClass);
export type ImageMethods = Partial<typeof Image>;

export const ImageMock = Object.assign(Mock, {
export const imageMethodsMock: ImageMethods = {
getSize: noop,
getSizeWithHeaders: noop,
prefetch: noop,
prefetchWithMetadata: noop,
queryCache: noop,
resolveAssetSource: noop,
});
prefetch: () => Promise.resolve(false),
prefetchWithMetadata: () => Promise.resolve(false),
queryCache: () => Promise.resolve({ }),
resolveAssetSource: () => ({
height: 0,
scale: 0,
uri: "",
width: 0,
}),
};

const Mock = mockComponent(Image as ComponentClass);

export const ImageMock = Object.assign(Mock, imageMethodsMock);
57 changes: 47 additions & 10 deletions src/lib/Components/ScrollView.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,62 @@
/* eslint-disable sort-keys */
import { PropsWithChildren, ReactNode, createElement } from "react";
import { ScrollView, View, requireNativeComponent } from "react-native";
import { ElementRef, PropsWithChildren, ReactNode, createElement } from "react";
import { HostComponent, NativeMethods, ScrollView, View, requireNativeComponent } from "react-native";

import { noop } from "../../helpers/commons";
import { mockComponent } from "../../helpers/mockComponent";
import { MockNativeMethods } from "../../helpers/mockNativeMethods";
import { nativeMethodsMock } from "../../helpers/nativeMethodsMock";

const RCTScrollView = requireNativeComponent("RCTScrollView");
const BaseMock = mockComponent(ScrollView, {
...MockNativeMethods,
getScrollResponder: noop,
export type ScrollViewMethods = NativeMethods | ScrollView & {
getInnerViewRef: () => ElementRef<typeof View> | null;
getNativeScrollRef: () => ElementRef<HostComponent<unknown>> | null;
};

export const scrollViewMethodsMock: ScrollViewMethods = {
...nativeMethodsMock,
getScrollResponder: () => ({
addListenerOn: noop,
componentWillMount: noop,
scrollResponderGetScrollableNode: noop,
scrollResponderHandleMomentumScrollBegin: noop,
scrollResponderHandleMomentumScrollEnd: noop,
scrollResponderHandleResponderGrant: noop,
scrollResponderHandleResponderReject: noop,
scrollResponderHandleResponderRelease: noop,
scrollResponderHandleScroll: noop,
scrollResponderHandleScrollBeginDrag: noop,
scrollResponderHandleScrollEndDrag: noop,
scrollResponderHandleScrollShouldSetResponder: () => false,
scrollResponderHandleStartShouldSetResponder: () => false,
scrollResponderHandleStartShouldSetResponderCapture: () => false,
scrollResponderHandleTerminationRequest: () => false,
scrollResponderHandleTouchEnd: noop,
scrollResponderHandleTouchMove: noop,
scrollResponderHandleTouchStart: noop,
scrollResponderInputMeasureAndScrollToKeyboard: noop,
scrollResponderIsAnimating: () => false,
scrollResponderKeyboardDidHide: noop,
scrollResponderKeyboardDidShow: noop,
scrollResponderKeyboardWillHide: noop,
scrollResponderKeyboardWillShow: noop,
scrollResponderScrollNativeHandleToKeyboard: noop,
scrollResponderScrollTo: noop,
scrollResponderTextInputFocusError: noop,
scrollResponderZoomTo: noop,
}),
getScrollableNode: noop,
getInnerViewNode: noop,
getInnerViewRef: noop,
getNativeScrollRef: noop,
getInnerViewRef: () => null,
getNativeScrollRef: () => null,
scrollTo: noop,
scrollToEnd: noop,
flashScrollIndicators: noop,
scrollResponderZoomTo: noop,
scrollResponderScrollNativeHandleToKeyboard: noop,
});

};

const RCTScrollView = requireNativeComponent("RCTScrollView");
const BaseMock = mockComponent(ScrollView, scrollViewMethodsMock);

export class ScrollViewMock<P> extends BaseMock {

Expand Down
4 changes: 2 additions & 2 deletions src/lib/Components/Text.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Text } from "react-native";

import { mockComponent } from "../../helpers/mockComponent";
import { MockNativeMethods } from "../../helpers/mockNativeMethods";
import { nativeMethodsMock } from "../../helpers/nativeMethodsMock";

export const TextMock = mockComponent(Text, MockNativeMethods);
export const TextMock = mockComponent(Text, nativeMethodsMock);
22 changes: 15 additions & 7 deletions src/lib/Components/TextInput.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { TextInput } from "react-native";
import { ElementRef } from "react";
import { HostComponent, NativeMethods, TextInput } from "react-native";

import { noop } from "../../helpers/commons";
import { mockComponent } from "../../helpers/mockComponent";
import { MockNativeMethods } from "../../helpers/mockNativeMethods";
import { nativeMethodsMock } from "../../helpers/nativeMethodsMock";

export const TextInputMock = mockComponent(TextInput, {
...MockNativeMethods,
export type TextInputMethods = NativeMethods | TextInput & {
getNativeRef: () => ElementRef<HostComponent<unknown>> | undefined;
};

export const textInputMethodsMock: TextInputMethods = {
...nativeMethodsMock,
clear: noop,
getNativeRef: noop,
isFocused: noop,
});
getNativeRef: () => undefined,
isFocused: () => false,
setSelection: noop,
};

export const TextInputMock = mockComponent(TextInput, textInputMethodsMock);
4 changes: 2 additions & 2 deletions src/lib/Components/View.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { View } from "react-native";

import { mockComponent } from "../../helpers/mockComponent";
import { MockNativeMethods } from "../../helpers/mockNativeMethods";
import { nativeMethodsMock } from "../../helpers/nativeMethodsMock";

export const ViewMock = mockComponent(View, MockNativeMethods);
export const ViewMock = mockComponent(View, nativeMethodsMock);
80 changes: 80 additions & 0 deletions src/lib/mockNative.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import type { ComponentClass, PropsWithChildren } from "react";
import type { NativeMethods } from "react-native";
import { match } from "ts-pattern";

import { replace } from "../helpers/commons";
import { AllNativeMethods, mockComponent } from "../helpers/mockComponent";
import { nativeMethodsMock } from "../helpers/nativeMethodsMock";

import { imageMethodsMock, ImageMethods } from "./Components/Image";
import { ScrollViewMethods, scrollViewMethodsMock } from "./Components/ScrollView";
import { TextInputMethods, textInputMethodsMock } from "./Components/TextInput";

export type NativeBase =
| "ActivityIndicator"
| "Modal"
| "Text"
| "View";

export type NativeKey = NativeBase
| "Image"
| "ScrollView"
| "TextInput";

const MOCKS: Set<NativeKey> = new Set();

const PATHS: Record<NativeKey, string> = {
ActivityIndicator: "react-native/Libraries/Components/ActivityIndicator/ActivityIndicator",
Image: "react-native/Libraries/Image/Image",
Modal: "react-native/Libraries/Modal/Modal",
ScrollView: "react-native/Libraries/Components/ScrollView/ScrollView",
Text: "react-native/Libraries/Text/Text",
TextInput: "react-native/Libraries/Components/TextInput/TextInput",
View: "react-native/Libraries/Components/View/View",
};

/**
* Allows you to change the behavior of native methods for certain components.
* You can restore all behaviors to their original mocks using
* {@link restoreNativeMocks} function.
*
* @param options type of component and native methods
*/
export function mockNative(type: "TextInput", methods: Partial<TextInputMethods>): void;
export function mockNative(type: "ScrollView", methods: Partial<ScrollViewMethods>): void;
export function mockNative(type: "Image", methods: Partial<ImageMethods>): void;
export function mockNative(type: NativeBase, methods: Partial<NativeMethods>): void;
export function mockNative(type: NativeKey, methods: Partial<AllNativeMethods | ImageMethods>): void {
const path = PATHS[type];
const Comp = require(path) as ComponentClass<PropsWithChildren<unknown>>;

const Mock = match(type)
.with("Image", () => Object.assign(Comp, { ...imageMethodsMock, ...methods }))
.with("ScrollView", () => mockComponent(Comp, Object.assign({ }, scrollViewMethodsMock, methods)))
.with("TextInput", () => mockComponent(Comp, Object.assign({ }, textInputMethodsMock, methods)))
.otherwise(() => mockComponent(Comp, Object.assign({ }, nativeMethodsMock, methods)));

replace(path, type === "ActivityIndicator" ? { default: Mock } : Mock);
MOCKS.add(type);
}

/**
* Restore the native methods behavior off all native components to their
* original mocks.
*/
export function restoreNativeMocks(): void {
MOCKS.forEach(type => {
const path = PATHS[type];
const Comp = require(path) as ComponentClass<PropsWithChildren<unknown>>;
const Mock = match(type)
.with("Image", () => Object.assign(Comp, imageMethodsMock))
.with("ScrollView", () => mockComponent(Comp, scrollViewMethodsMock))
.with("TextInput", () => mockComponent(Comp, textInputMethodsMock))
.otherwise(() => mockComponent(Comp, nativeMethodsMock));

replace(path, type === "ActivityIndicator" ? { default: Mock } : Mock);
});

MOCKS.clear();
}
40 changes: 6 additions & 34 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,6 @@
import "./lib/babelRegister";
import "./lib/polyfills";
import "./lib/coreMocks";

import { replace } from "./helpers/commons";
import { AccessibilityInfoMock } from "./lib/Components/AccessibilityInfo";
import { ActivityIndicatorMock } from "./lib/Components/ActivityIndicator";
import { AppStateMock } from "./lib/Components/AppState";
import { ClipboardMock } from "./lib/Components/Clipboard";
import { ImageMock } from "./lib/Components/Image";
import { LinkingMock } from "./lib/Components/Linking";
import { ModalMock } from "./lib/Components/Modal";
import { RefreshControlMock } from "./lib/Components/RefreshControl";
import { ScrollViewMock } from "./lib/Components/ScrollView";
import { TextMock } from "./lib/Components/Text";
import { TextInputMock } from "./lib/Components/TextInput";
import { VibrationMock } from "./lib/Components/Vibration";
import { ViewMock } from "./lib/Components/View";
import { ViewNativeComponentMock } from "./lib/Components/ViewNativeComponent";

replace("react-native/Libraries/Image/Image", ImageMock);
replace("react-native/Libraries/Text/Text", TextMock);
replace("react-native/Libraries/Components/TextInput/TextInput", TextInputMock);
replace("react-native/Libraries/Modal/Modal", ModalMock);
replace("react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo", { default: AccessibilityInfoMock });
replace("react-native/Libraries/Components/Clipboard/Clipboard", ClipboardMock);
replace("react-native/Libraries/Components/RefreshControl/RefreshControl", RefreshControlMock);
replace("react-native/Libraries/Components/ScrollView/ScrollView", ScrollViewMock);
replace("react-native/Libraries/Components/ActivityIndicator/ActivityIndicator", { default: ActivityIndicatorMock });
replace("react-native/Libraries/AppState/AppState", AppStateMock);
replace("react-native/Libraries/Linking/Linking", LinkingMock);
replace("react-native/Libraries/Vibration/Vibration", VibrationMock);
replace("react-native/Libraries/Components/View/View", ViewMock);
replace("react-native/Libraries/Components/View/ViewNativeComponent", ViewNativeComponentMock);
export {
NativeBase,
NativeKey,
mockNative,
restoreNativeMocks,
} from "./lib/mockNative";
34 changes: 34 additions & 0 deletions src/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import "./lib/babelRegister";
import "./lib/polyfills";
import "./lib/coreMocks";

import { replace } from "./helpers/commons";
import { AccessibilityInfoMock } from "./lib/Components/AccessibilityInfo";
import { ActivityIndicatorMock } from "./lib/Components/ActivityIndicator";
import { AppStateMock } from "./lib/Components/AppState";
import { ClipboardMock } from "./lib/Components/Clipboard";
import { ImageMock } from "./lib/Components/Image";
import { LinkingMock } from "./lib/Components/Linking";
import { ModalMock } from "./lib/Components/Modal";
import { RefreshControlMock } from "./lib/Components/RefreshControl";
import { ScrollViewMock } from "./lib/Components/ScrollView";
import { TextMock } from "./lib/Components/Text";
import { TextInputMock } from "./lib/Components/TextInput";
import { VibrationMock } from "./lib/Components/Vibration";
import { ViewMock } from "./lib/Components/View";
import { ViewNativeComponentMock } from "./lib/Components/ViewNativeComponent";

replace("react-native/Libraries/Image/Image", ImageMock);
replace("react-native/Libraries/Text/Text", TextMock);
replace("react-native/Libraries/Components/TextInput/TextInput", TextInputMock);
replace("react-native/Libraries/Modal/Modal", ModalMock);
replace("react-native/Libraries/Components/AccessibilityInfo/AccessibilityInfo", { default: AccessibilityInfoMock });
replace("react-native/Libraries/Components/Clipboard/Clipboard", ClipboardMock);
replace("react-native/Libraries/Components/RefreshControl/RefreshControl", RefreshControlMock);
replace("react-native/Libraries/Components/ScrollView/ScrollView", ScrollViewMock);
replace("react-native/Libraries/Components/ActivityIndicator/ActivityIndicator", { default: ActivityIndicatorMock });
replace("react-native/Libraries/AppState/AppState", AppStateMock);
replace("react-native/Libraries/Linking/Linking", LinkingMock);
replace("react-native/Libraries/Vibration/Vibration", VibrationMock);
replace("react-native/Libraries/Components/View/View", ViewMock);
replace("react-native/Libraries/Components/View/ViewNativeComponent", ViewNativeComponentMock);
Loading
Loading