diff --git a/package.json b/package.json
index a64cdda..b8f6074 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/register.js b/register.js
index e1bdfa0..9567096 100644
--- a/register.js
+++ b/register.js
@@ -1,5 +1,5 @@
const start = Date.now();
-require("./dist/main");
+require("./dist/register");
const end = Date.now();
const diff = (end - start) / 1000;
diff --git a/src/helpers/mockComponent.ts b/src/helpers/mockComponent.ts
index 6f2b40e..23706eb 100644
--- a/src/helpers/mockComponent.ts
+++ b/src/helpers/mockComponent.ts
@@ -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
>>(
RealComponent: C,
- instanceMethods?: object | null,
+ instanceMethods?: AllNativeMethods,
): C {
const SuperClass: ComponentClass> = typeof RealComponent === "function"
? RealComponent
diff --git a/src/helpers/mockNativeMethods.ts b/src/helpers/nativeMethodsMock.ts
similarity index 58%
rename from src/helpers/mockNativeMethods.ts
rename to src/helpers/nativeMethodsMock.ts
index 0f68a49..6986381 100644
--- a/src/helpers/mockNativeMethods.ts
+++ b/src/helpers/nativeMethodsMock.ts
@@ -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,
};
diff --git a/src/lib/Components/ActivityIndicator.ts b/src/lib/Components/ActivityIndicator.ts
index 8463563..932730b 100644
--- a/src/lib/Components/ActivityIndicator.ts
+++ b/src/lib/Components/ActivityIndicator.ts
@@ -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);
diff --git a/src/lib/Components/Image.ts b/src/lib/Components/Image.ts
index 53d14e4..1236ca6 100644
--- a/src/lib/Components/Image.ts
+++ b/src/lib/Components/Image.ts
@@ -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;
-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);
diff --git a/src/lib/Components/ScrollView.ts b/src/lib/Components/ScrollView.ts
index 8c4b595..26d3407 100644
--- a/src/lib/Components/ScrollView.ts
+++ b/src/lib/Components/ScrollView.ts
@@ -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 | null;
+ getNativeScrollRef: () => ElementRef> | 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 extends BaseMock {
diff --git a/src/lib/Components/Text.ts b/src/lib/Components/Text.ts
index 2aaa993..5042776 100644
--- a/src/lib/Components/Text.ts
+++ b/src/lib/Components/Text.ts
@@ -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);
diff --git a/src/lib/Components/TextInput.ts b/src/lib/Components/TextInput.ts
index 1ebba3e..1ce29f4 100644
--- a/src/lib/Components/TextInput.ts
+++ b/src/lib/Components/TextInput.ts
@@ -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> | undefined;
+};
+
+export const textInputMethodsMock: TextInputMethods = {
+ ...nativeMethodsMock,
clear: noop,
- getNativeRef: noop,
- isFocused: noop,
-});
+ getNativeRef: () => undefined,
+ isFocused: () => false,
+ setSelection: noop,
+};
+
+export const TextInputMock = mockComponent(TextInput, textInputMethodsMock);
diff --git a/src/lib/Components/View.ts b/src/lib/Components/View.ts
index 069ade7..59e9d5b 100644
--- a/src/lib/Components/View.ts
+++ b/src/lib/Components/View.ts
@@ -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);
diff --git a/src/lib/mockNative.ts b/src/lib/mockNative.ts
new file mode 100644
index 0000000..e72f49d
--- /dev/null
+++ b/src/lib/mockNative.ts
@@ -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 = new Set();
+
+const PATHS: Record = {
+ 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): void;
+export function mockNative(type: "ScrollView", methods: Partial): void;
+export function mockNative(type: "Image", methods: Partial): void;
+export function mockNative(type: NativeBase, methods: Partial): void;
+export function mockNative(type: NativeKey, methods: Partial): void {
+ const path = PATHS[type];
+ const Comp = require(path) as ComponentClass>;
+
+ 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>;
+ 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();
+}
diff --git a/src/main.ts b/src/main.ts
index 334404d..ee04199 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -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";
diff --git a/src/register.ts b/src/register.ts
new file mode 100644
index 0000000..334404d
--- /dev/null
+++ b/src/register.ts
@@ -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);
diff --git a/test/unit/lib/mockNative.test.tsx b/test/unit/lib/mockNative.test.tsx
new file mode 100644
index 0000000..834bab8
--- /dev/null
+++ b/test/unit/lib/mockNative.test.tsx
@@ -0,0 +1,53 @@
+import "../../../src/register";
+
+import { expect } from "@assertive-ts/core";
+import { render } from "@testing-library/react-native";
+import { ReactElement, useEffect, useRef, useState } from "react";
+import { Text, View } from "react-native";
+
+import { mockNative, restoreNativeMocks } from "../../../src/lib/mockNative";
+
+function TestScreen(): ReactElement {
+
+ const [widthValue, setWidthValue] = useState(0);
+ const viewRef = useRef(null);
+
+ useEffect(() => {
+ viewRef.current?.measure((_x, _y, width) => {
+ setWidthValue(width);
+ });
+ }, []);
+
+ return (
+
+ {`Measured width: ${widthValue}`}
+
+ );
+}
+
+describe("[Unit] mockNative.test.tsx", () => {
+ afterEach(restoreNativeMocks);
+
+ describe(".mockNative", () => {
+ it("change the behavior of native methods", () => {
+ mockNative("View", { measure: cb => cb(0, 0, 50, 0, 0, 0) });
+ const { getByText } = render();
+
+ expect(getByText("Measured width: 50")).toBePresent();
+ });
+ });
+
+ describe(".restoreNativeMocks", () => {
+ it("restores the behavior of native methods to their original mocks", () => {
+ mockNative("View", { measure: cb => cb(0, 0, 100, 0, 0, 0) });
+ const init = render();
+
+ expect(init.getByText("Measured width: 100")).toBePresent();
+
+ restoreNativeMocks();
+ const next = render();
+
+ expect(next.getByText("Measured width: 0")).toBePresent();
+ });
+ });
+});
diff --git a/test/unit/main.test.tsx b/test/unit/main.test.tsx
deleted file mode 100644
index eecb98d..0000000
--- a/test/unit/main.test.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import "../../src/main";
-
-import { expect } from "@assertive-ts/core";
-import { render } from "@testing-library/react-native";
-import { Text, View } from "react-native";
-
-describe("[Unit] main.test.ts", () => {
- context("when main is called", () => {
- it("mocks react native so it can render on Node.js", () => {
- const { getByText } = render(
-
- {"Hello world!"}
- ,
- );
-
- expect(getByText("Hello world!")).toBePresent();
- expect(() => getByText("foo")).toThrowError();
- });
- });
-});
diff --git a/test/unit/register.test.tsx b/test/unit/register.test.tsx
new file mode 100644
index 0000000..9c437f6
--- /dev/null
+++ b/test/unit/register.test.tsx
@@ -0,0 +1,41 @@
+import "../../src/register";
+
+import { expect } from "@assertive-ts/core";
+import { render } from "@testing-library/react-native";
+import { ActivityIndicator, Image, Modal, ScrollView, Text, TextInput, View } from "react-native";
+
+describe("[Unit] register.test.ts", () => {
+ context("when main is called", () => {
+ it("mocks react native so it can render on Node.js", () => {
+ const {
+ getByText,
+ getByPlaceholderText,
+ getByDisplayValue,
+ getByLabelText,
+ } = render(
+
+
+
+ {"Hello world!"}
+
+
+
+ {"I'm on a modal"}
+
+
+ {"foo"}
+
+
+ ,
+ );
+
+ expect(getByLabelText("Loading")).toBePresent();
+ expect(getByText("Hello world!")).toBePresent();
+ expect(getByPlaceholderText("Say hello here...")).toBePresent();
+ expect(getByDisplayValue("Hello :)")).toBePresent();
+ expect(getByLabelText("Profile picture")).toBePresent();
+ expect(getByText("I'm on a modal")).toBePresent();
+ expect(() => getByText("foo")).toThrowError();
+ });
+ });
+});
diff --git a/yarn.lock b/yarn.lock
index b112428..6c1f9ea 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8881,6 +8881,7 @@ __metadata:
semantic-release-yarn: "npm:^3.0.2"
sinon: "npm:^17.0.1"
ts-node: "npm:^10.9.2"
+ ts-pattern: "npm:^5.0.8"
tslib: "npm:^2.6.2"
typescript: "npm:^5.3.3"
peerDependencies:
@@ -10376,6 +10377,13 @@ __metadata:
languageName: node
linkType: hard
+"ts-pattern@npm:^5.0.8":
+ version: 5.0.8
+ resolution: "ts-pattern@npm:5.0.8"
+ checksum: 10/a57f7def89c0fae3065d56a1fb94510688910b4b99610a41b2b3df277d92af17c26a4a65717e7d7c3ac085258d20144f2f5a748d860c043609aea29cff7a43ab
+ languageName: node
+ linkType: hard
+
"tsconfig-paths@npm:^3.15.0":
version: 3.15.0
resolution: "tsconfig-paths@npm:3.15.0"