@solucx/react-native-solucx-widget 0.1.16 → 0.2.1
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/package.json +31 -4
- package/src/SoluCXWidget.tsx +108 -53
- package/src/__mocks__/expo-modules-core-web.js +16 -0
- package/src/__mocks__/expo-modules-core.js +33 -0
- package/src/__tests__/ClientVersionCollector.test.ts +55 -0
- package/src/__tests__/CloseButton.test.tsx +47 -0
- package/src/__tests__/Constants.test.ts +17 -0
- package/src/__tests__/InlineWidget.rendering.test.tsx +81 -0
- package/src/__tests__/ModalWidget.rendering.test.tsx +157 -0
- package/src/__tests__/OverlayWidget.rendering.test.tsx +123 -0
- package/src/__tests__/SoluCXWidget.rendering.test.tsx +504 -0
- package/src/__tests__/e2e/widget-lifecycle.test.tsx +352 -0
- package/src/__tests__/integration/webview-communication-simple.test.tsx +147 -0
- package/src/__tests__/integration/webview-communication.test.tsx +417 -0
- package/src/__tests__/useDeviceInfoCollector.test.ts +109 -0
- package/src/__tests__/useWidgetState.test.ts +76 -84
- package/src/__tests__/widgetBootstrapService.test.ts +182 -0
- package/src/components/ModalWidget.tsx +3 -5
- package/src/components/OverlayWidget.tsx +1 -1
- package/src/constants/Constants.ts +4 -0
- package/src/constants/webViewConstants.ts +1 -0
- package/src/hooks/useDeviceInfoCollector.ts +67 -0
- package/src/hooks/useWidgetState.ts +4 -4
- package/src/index.ts +4 -0
- package/src/interfaces/WidgetCallbacks.ts +14 -0
- package/src/interfaces/index.ts +3 -2
- package/src/services/ClientVersionCollector.ts +15 -0
- package/src/services/storage.ts +2 -2
- package/src/services/widgetBootstrapService.ts +67 -0
- package/src/services/widgetEventService.ts +14 -30
- package/src/services/widgetValidationService.ts +29 -13
- package/src/setupTests.js +43 -0
- package/src/styles/widgetStyles.ts +1 -1
- package/src/utils/urlUtils.ts +2 -2
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solucx/react-native-solucx-widget",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "The React Native SDK for Solucx Widget",
|
|
5
5
|
"main": "src/index",
|
|
6
6
|
"author": " <> ()",
|
|
7
7
|
"homepage": "#readme",
|
|
8
8
|
"scripts": {
|
|
9
9
|
"build": "tsc",
|
|
10
|
-
"prepublishOnly": "npm run build"
|
|
10
|
+
"prepublishOnly": "npm run build",
|
|
11
|
+
"test": "jest"
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
13
14
|
"src/",
|
|
@@ -17,7 +18,33 @@
|
|
|
17
18
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
|
18
19
|
"react": ">=18.0.0",
|
|
19
20
|
"react-native": ">=0.72.0",
|
|
20
|
-
"react-native-
|
|
21
|
-
"react-native-
|
|
21
|
+
"react-native-safe-area-context": "^5.6.1",
|
|
22
|
+
"react-native-webview": "^13.16.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@babel/core": "^7.25.2",
|
|
26
|
+
"@testing-library/react-native": "^13.3.3",
|
|
27
|
+
"@types/jest": "29.5.14",
|
|
28
|
+
"@types/react": "~19.1.10",
|
|
29
|
+
"@types/react-native": "^0.72.8",
|
|
30
|
+
"jest": "~29.7.0",
|
|
31
|
+
"jest-expo": "~54.0.17",
|
|
32
|
+
"react-test-renderer": "19.1.0",
|
|
33
|
+
"typescript": "~5.9.2"
|
|
34
|
+
},
|
|
35
|
+
"jest": {
|
|
36
|
+
"preset": "jest-expo",
|
|
37
|
+
"setupFilesAfterEnv": [
|
|
38
|
+
"<rootDir>/src/setupTests.js"
|
|
39
|
+
],
|
|
40
|
+
"testPathIgnorePatterns": [
|
|
41
|
+
"/lib/"
|
|
42
|
+
],
|
|
43
|
+
"moduleNameMapper": {
|
|
44
|
+
"expo-modules-core/src/Refs": "<rootDir>/src/__mocks__/expo-modules-core.js",
|
|
45
|
+
"expo-modules-core/src/web/index.web": "<rootDir>/src/__mocks__/expo-modules-core-web.js",
|
|
46
|
+
"expo-modules-core/src/uuid/uuid.web": "<rootDir>/src/__mocks__/expo-modules-core.js",
|
|
47
|
+
"expo-modules-core$": "<rootDir>/src/__mocks__/expo-modules-core.js"
|
|
48
|
+
}
|
|
22
49
|
}
|
|
23
50
|
}
|
package/src/SoluCXWidget.tsx
CHANGED
|
@@ -1,118 +1,173 @@
|
|
|
1
|
-
import React, { useEffect, useRef, useCallback } from
|
|
2
|
-
import { Dimensions } from
|
|
3
|
-
import { WebView } from
|
|
4
|
-
|
|
5
|
-
import { SoluCXKey, WidgetData, WidgetOptions, WidgetType } from
|
|
6
|
-
import { useWidgetState } from
|
|
7
|
-
import { WidgetEventService } from
|
|
8
|
-
import {
|
|
9
|
-
import { WEB_VIEW_MESSAGE_LISTENER } from
|
|
10
|
-
import { ModalWidget } from
|
|
11
|
-
import { InlineWidget } from
|
|
12
|
-
import { OverlayWidget } from
|
|
1
|
+
import React, { useEffect, useRef, useCallback, useState, useMemo } from "react";
|
|
2
|
+
import { Dimensions } from "react-native";
|
|
3
|
+
import { WebView } from "react-native-webview";
|
|
4
|
+
|
|
5
|
+
import type { SoluCXKey, WidgetData, WidgetOptions, WidgetType, WidgetCallbacks } from "./interfaces";
|
|
6
|
+
import { useWidgetState } from "./hooks/useWidgetState";
|
|
7
|
+
import { WidgetEventService } from "./services/widgetEventService";
|
|
8
|
+
import { WidgetValidationService } from "./services/widgetValidationService";
|
|
9
|
+
import { WEB_VIEW_MESSAGE_LISTENER } from "./constants/webViewConstants";
|
|
10
|
+
import { ModalWidget } from "./components/ModalWidget";
|
|
11
|
+
import { InlineWidget } from "./components/InlineWidget";
|
|
12
|
+
import { OverlayWidget } from "./components/OverlayWidget";
|
|
13
|
+
import { requestWidgetUrl } from "./services/widgetBootstrapService";
|
|
13
14
|
|
|
14
15
|
interface SoluCXWidgetProps {
|
|
15
16
|
soluCXKey: SoluCXKey;
|
|
16
17
|
type: WidgetType;
|
|
17
18
|
data: WidgetData;
|
|
18
19
|
options: WidgetOptions;
|
|
20
|
+
callbacks?: WidgetCallbacks;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
export const SoluCXWidget: React.FC<SoluCXWidgetProps> = ({
|
|
22
|
-
soluCXKey,
|
|
23
|
-
type,
|
|
24
|
-
data,
|
|
25
|
-
options
|
|
26
|
-
}) => {
|
|
23
|
+
export const SoluCXWidget: React.FC<SoluCXWidgetProps> = ({ soluCXKey, type, data, options, callbacks }) => {
|
|
27
24
|
const webviewRef = useRef<WebView>(null);
|
|
28
|
-
const { width } = Dimensions.get(
|
|
25
|
+
const { width } = Dimensions.get("window");
|
|
26
|
+
|
|
27
|
+
if (!data) {
|
|
28
|
+
callbacks?.onError?.("Widget data is required but was not provided");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
29
31
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
isWidgetVisible,
|
|
33
|
-
setIsWidgetVisible,
|
|
34
|
-
loadSavedData,
|
|
35
|
-
resize,
|
|
36
|
-
open,
|
|
37
|
-
close,
|
|
38
|
-
userId,
|
|
39
|
-
} = useWidgetState(data, options, type);
|
|
32
|
+
const serializedData = JSON.stringify(data);
|
|
33
|
+
const normalizedData = useMemo(() => data, [serializedData]);
|
|
40
34
|
|
|
41
|
-
const
|
|
35
|
+
const { widgetHeight, isWidgetVisible, setIsWidgetVisible, loadSavedData, resize, open, close, userId } =
|
|
36
|
+
useWidgetState(normalizedData, options, type);
|
|
37
|
+
|
|
38
|
+
const eventService = useMemo(
|
|
39
|
+
() => new WidgetEventService(setIsWidgetVisible, resize, userId, callbacks),
|
|
40
|
+
[setIsWidgetVisible, resize],
|
|
41
|
+
);
|
|
42
42
|
|
|
43
|
-
const
|
|
44
|
-
const isForm = Boolean(
|
|
43
|
+
const validationService = useMemo(() => new WidgetValidationService(userId), [userId]);
|
|
44
|
+
const isForm = Boolean(normalizedData.form_id);
|
|
45
|
+
const [widgetUri, setWidgetUri] = useState<string | null>(null);
|
|
45
46
|
|
|
46
47
|
useEffect(() => {
|
|
47
48
|
loadSavedData();
|
|
48
49
|
}, [loadSavedData]);
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
let isActive = true;
|
|
53
|
+
|
|
54
|
+
const prepareWidgetURL = async () => {
|
|
55
|
+
setWidgetUri(null);
|
|
56
|
+
|
|
52
57
|
try {
|
|
53
|
-
await
|
|
58
|
+
const widgetUrl = await requestWidgetUrl(soluCXKey, normalizedData, userId);
|
|
59
|
+
if (!isActive || !widgetUrl) return;
|
|
60
|
+
|
|
61
|
+
const result = await validationService.shouldDisplayWidget(options);
|
|
62
|
+
if (!isActive) return;
|
|
63
|
+
|
|
64
|
+
if (!result.canDisplay) {
|
|
65
|
+
const blockReason = result?.blockReason;
|
|
66
|
+
callbacks?.onBlock?.(blockReason);
|
|
67
|
+
setIsWidgetVisible(false);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
callbacks?.onPreOpen?.(userId);
|
|
72
|
+
open();
|
|
73
|
+
setWidgetUri(widgetUrl);
|
|
74
|
+
callbacks?.onOpened?.(userId);
|
|
54
75
|
} catch (error) {
|
|
55
|
-
|
|
76
|
+
if (isActive) {
|
|
77
|
+
let errorMessage = "Unknown error";
|
|
78
|
+
|
|
79
|
+
if (error instanceof Error) errorMessage = error.message;
|
|
80
|
+
else if (typeof error === "string") errorMessage = error;
|
|
81
|
+
else if (error !== null && typeof error === "object") errorMessage = JSON.stringify(error);
|
|
82
|
+
|
|
83
|
+
callbacks?.onError?.(errorMessage);
|
|
84
|
+
setIsWidgetVisible(false);
|
|
85
|
+
}
|
|
56
86
|
}
|
|
57
|
-
}
|
|
58
|
-
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
prepareWidgetURL();
|
|
90
|
+
|
|
91
|
+
return () => {
|
|
92
|
+
isActive = false;
|
|
93
|
+
};
|
|
94
|
+
}, [validationService, soluCXKey, normalizedData, isForm, setIsWidgetVisible, options, open, userId, callbacks]);
|
|
95
|
+
|
|
96
|
+
const handleWebViewMessage = useCallback(
|
|
97
|
+
async (message: string) => {
|
|
98
|
+
if (message && message.length > 0) {
|
|
99
|
+
try {
|
|
100
|
+
await eventService.handleMessage(message, isForm);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error("Error handling widget message:", error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
[eventService, isForm],
|
|
107
|
+
);
|
|
59
108
|
|
|
60
109
|
const handleWebViewLoad = useCallback(() => {
|
|
61
110
|
webviewRef.current?.injectJavaScript(WEB_VIEW_MESSAGE_LISTENER);
|
|
62
111
|
}, []);
|
|
63
112
|
|
|
64
113
|
const handleClose = useCallback(() => {
|
|
65
|
-
if (type ===
|
|
114
|
+
if (type === "inline" || type === "modal") {
|
|
66
115
|
close();
|
|
67
116
|
}
|
|
68
117
|
setIsWidgetVisible(false);
|
|
69
|
-
}, [setIsWidgetVisible]);
|
|
118
|
+
}, [setIsWidgetVisible, close, type]);
|
|
70
119
|
|
|
120
|
+
const webViewStyle = [{ height: widgetHeight }, { width }];
|
|
71
121
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
];
|
|
122
|
+
if (!widgetUri) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
76
125
|
|
|
77
|
-
if (type ===
|
|
126
|
+
if (type === "modal") {
|
|
78
127
|
return (
|
|
79
128
|
<ModalWidget visible={isWidgetVisible} height={widgetHeight} onClose={handleClose}>
|
|
80
129
|
<WebView
|
|
81
130
|
ref={webviewRef}
|
|
82
131
|
style={webViewStyle}
|
|
83
|
-
source={{ uri }}
|
|
132
|
+
source={{ uri: widgetUri }}
|
|
84
133
|
onLoadEnd={handleWebViewLoad}
|
|
85
134
|
onMessage={(event) => handleWebViewMessage(event.nativeEvent.data)}
|
|
86
|
-
originWhitelist={[
|
|
135
|
+
originWhitelist={["*"]}
|
|
87
136
|
/>
|
|
88
137
|
</ModalWidget>
|
|
89
138
|
);
|
|
90
139
|
}
|
|
91
140
|
|
|
92
|
-
if (type ===
|
|
141
|
+
if (type === "inline") {
|
|
93
142
|
return (
|
|
94
143
|
<InlineWidget visible={isWidgetVisible} height={widgetHeight} onClose={handleClose}>
|
|
95
144
|
<WebView
|
|
96
145
|
ref={webviewRef}
|
|
97
146
|
style={webViewStyle}
|
|
98
|
-
source={{ uri }}
|
|
147
|
+
source={{ uri: widgetUri }}
|
|
99
148
|
onLoadEnd={handleWebViewLoad}
|
|
100
149
|
onMessage={(event) => handleWebViewMessage(event.nativeEvent.data)}
|
|
101
|
-
originWhitelist={[
|
|
150
|
+
originWhitelist={["*"]}
|
|
102
151
|
/>
|
|
103
152
|
</InlineWidget>
|
|
104
153
|
);
|
|
105
154
|
}
|
|
106
155
|
|
|
107
156
|
return (
|
|
108
|
-
<OverlayWidget
|
|
157
|
+
<OverlayWidget
|
|
158
|
+
visible={isWidgetVisible}
|
|
159
|
+
width={width}
|
|
160
|
+
height={widgetHeight}
|
|
161
|
+
position={type}
|
|
162
|
+
onClose={handleClose}
|
|
163
|
+
>
|
|
109
164
|
<WebView
|
|
110
165
|
ref={webviewRef}
|
|
111
166
|
style={webViewStyle}
|
|
112
|
-
source={{ uri }}
|
|
167
|
+
source={{ uri: widgetUri }}
|
|
113
168
|
onLoadEnd={handleWebViewLoad}
|
|
114
169
|
onMessage={(event) => handleWebViewMessage(event.nativeEvent.data)}
|
|
115
|
-
originWhitelist={[
|
|
170
|
+
originWhitelist={["*"]}
|
|
116
171
|
/>
|
|
117
172
|
</OverlayWidget>
|
|
118
173
|
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Mock for expo-modules-core/src/web/index.web
|
|
2
|
+
// Provides stubs for web globals normally installed via JSI
|
|
3
|
+
|
|
4
|
+
if (typeof globalThis.expo === 'undefined') {
|
|
5
|
+
globalThis.expo = {
|
|
6
|
+
EventEmitter: class EventEmitter {
|
|
7
|
+
constructor() { this._listeners = {}; }
|
|
8
|
+
addListener() { return { remove: () => {} }; }
|
|
9
|
+
removeAllListeners() {}
|
|
10
|
+
emit() {}
|
|
11
|
+
},
|
|
12
|
+
NativeModule: class NativeModule {},
|
|
13
|
+
SharedObject: class SharedObject {},
|
|
14
|
+
modules: {},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Comprehensive mock for expo-modules-core
|
|
2
|
+
const EventEmitter = class EventEmitter {
|
|
3
|
+
constructor() { this._listeners = {}; }
|
|
4
|
+
addListener() { return { remove: () => {} }; }
|
|
5
|
+
removeAllListeners() {}
|
|
6
|
+
emit() {}
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const NativeModule = class NativeModule {};
|
|
10
|
+
const SharedObject = class SharedObject {};
|
|
11
|
+
|
|
12
|
+
const createSnapshotFriendlyRef = () => {
|
|
13
|
+
const ref = { current: null };
|
|
14
|
+
Object.defineProperty(ref, 'toJSON', {
|
|
15
|
+
value: () => '[React.ref]',
|
|
16
|
+
});
|
|
17
|
+
return ref;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
EventEmitter,
|
|
22
|
+
NativeModule,
|
|
23
|
+
SharedObject,
|
|
24
|
+
NativeModulesProxy: {},
|
|
25
|
+
requireOptionalNativeModule: () => null,
|
|
26
|
+
requireNativeModule: (name) => { throw new Error(`Cannot find native module '${name}'`); },
|
|
27
|
+
requireNativeViewManager: () => ({}),
|
|
28
|
+
uuid: {
|
|
29
|
+
v4: () => 'mock-uuid-v4',
|
|
30
|
+
v5: () => 'mock-uuid-v5',
|
|
31
|
+
},
|
|
32
|
+
createSnapshotFriendlyRef,
|
|
33
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
describe('ClientVersionCollector', () => {
|
|
2
|
+
beforeEach(() => {
|
|
3
|
+
jest.resetModules();
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
it('should get version from react-native-device-info default export when available', () => {
|
|
7
|
+
jest.doMock('react-native-device-info', () => ({
|
|
8
|
+
default: {
|
|
9
|
+
getVersion: () => '3.0.0',
|
|
10
|
+
},
|
|
11
|
+
}), { virtual: true });
|
|
12
|
+
|
|
13
|
+
const { getClientVersion } = require('../services/ClientVersionCollector');
|
|
14
|
+
expect(getClientVersion()).toBe('3.0.0');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should get version from react-native-device-info direct export when default is not available', () => {
|
|
18
|
+
jest.doMock('react-native-device-info', () => ({
|
|
19
|
+
getVersion: () => '4.0.0',
|
|
20
|
+
}), { virtual: true });
|
|
21
|
+
|
|
22
|
+
const { getClientVersion } = require('../services/ClientVersionCollector');
|
|
23
|
+
expect(getClientVersion()).toBe('4.0.0');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should return "unknown" when react-native-device-info is not available', () => {
|
|
27
|
+
jest.doMock('react-native-device-info', () => {
|
|
28
|
+
throw new Error('Module not found');
|
|
29
|
+
}, { virtual: true });
|
|
30
|
+
|
|
31
|
+
const { getClientVersion } = require('../services/ClientVersionCollector');
|
|
32
|
+
expect(getClientVersion()).toBe('unknown');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should return "unknown" when react-native-device-info has no getVersion method', () => {
|
|
36
|
+
jest.doMock('react-native-device-info', () => ({
|
|
37
|
+
default: {},
|
|
38
|
+
}), { virtual: true });
|
|
39
|
+
|
|
40
|
+
const { getClientVersion } = require('../services/ClientVersionCollector');
|
|
41
|
+
expect(getClientVersion()).toBe('unknown');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should prefer default.getVersion when both default and direct getVersion are available', () => {
|
|
45
|
+
jest.doMock('react-native-device-info', () => ({
|
|
46
|
+
default: {
|
|
47
|
+
getVersion: () => '5.0.0',
|
|
48
|
+
},
|
|
49
|
+
getVersion: () => '6.0.0',
|
|
50
|
+
}), { virtual: true });
|
|
51
|
+
|
|
52
|
+
const { getClientVersion } = require('../services/ClientVersionCollector');
|
|
53
|
+
expect(getClientVersion()).toBe('5.0.0');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, fireEvent } from '@testing-library/react-native';
|
|
3
|
+
import { CloseButton } from '../components/CloseButton';
|
|
4
|
+
|
|
5
|
+
describe('CloseButton', () => {
|
|
6
|
+
it('should render close button text when visible is true', () => {
|
|
7
|
+
const { getByText } = render(
|
|
8
|
+
<CloseButton onPress={jest.fn()} visible={true} />
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
expect(getByText('✕')).toBeTruthy();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should not render when visible is false', () => {
|
|
15
|
+
const { queryByText } = render(
|
|
16
|
+
<CloseButton onPress={jest.fn()} visible={false} />
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
expect(queryByText('✕')).toBeNull();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should render by default when visible prop is omitted', () => {
|
|
23
|
+
const { getByText } = render(
|
|
24
|
+
<CloseButton onPress={jest.fn()} />
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
expect(getByText('✕')).toBeTruthy();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should call onPress when pressed', () => {
|
|
31
|
+
const mockOnPress = jest.fn();
|
|
32
|
+
const { getByText } = render(
|
|
33
|
+
<CloseButton onPress={mockOnPress} visible={true} />
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
fireEvent.press(getByText('✕'));
|
|
37
|
+
|
|
38
|
+
expect(mockOnPress).toHaveBeenCalledTimes(1);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should not call onPress when not pressed', () => {
|
|
42
|
+
const mockOnPress = jest.fn();
|
|
43
|
+
render(<CloseButton onPress={mockOnPress} visible={true} />);
|
|
44
|
+
|
|
45
|
+
expect(mockOnPress).not.toHaveBeenCalled();
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { SDK_NAME, SDK_VERSION } from '../constants/Constants';
|
|
2
|
+
|
|
3
|
+
describe('Constants', () => {
|
|
4
|
+
describe('SDK_NAME', () => {
|
|
5
|
+
it('should have the correct SDK name', () => {
|
|
6
|
+
expect(SDK_NAME).toBe('rn-widget-sdk');
|
|
7
|
+
});
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('SDK_VERSION', () => {
|
|
11
|
+
it('should import version from package.json', () => {
|
|
12
|
+
expect(SDK_VERSION).toBeDefined();
|
|
13
|
+
expect(typeof SDK_VERSION).toBe('string');
|
|
14
|
+
expect(SDK_VERSION).toMatch(/^\d+\.\d+\.\d+$/); // Verifica formato semver
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, fireEvent } from '@testing-library/react-native';
|
|
3
|
+
import { Text } from 'react-native';
|
|
4
|
+
import { InlineWidget } from '../components/InlineWidget';
|
|
5
|
+
|
|
6
|
+
describe('InlineWidget rendering', () => {
|
|
7
|
+
it('should render children when visible is true', () => {
|
|
8
|
+
const { getByText } = render(
|
|
9
|
+
<InlineWidget visible={true} height={300}>
|
|
10
|
+
<Text>Inline Survey</Text>
|
|
11
|
+
</InlineWidget>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
expect(getByText('Inline Survey')).toBeTruthy();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should render close button when visible is true', () => {
|
|
18
|
+
const { getByText } = render(
|
|
19
|
+
<InlineWidget visible={true} height={300}>
|
|
20
|
+
<Text>Content</Text>
|
|
21
|
+
</InlineWidget>
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(getByText('✕')).toBeTruthy();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should call onClose when close button is pressed', () => {
|
|
28
|
+
const mockOnClose = jest.fn();
|
|
29
|
+
const { getByText } = render(
|
|
30
|
+
<InlineWidget visible={true} height={300} onClose={mockOnClose}>
|
|
31
|
+
<Text>Content</Text>
|
|
32
|
+
</InlineWidget>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
fireEvent.press(getByText('✕'));
|
|
36
|
+
|
|
37
|
+
expect(mockOnClose).toHaveBeenCalledTimes(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should still render content when visible is false', () => {
|
|
41
|
+
const { getByText } = render(
|
|
42
|
+
<InlineWidget visible={false} height={300}>
|
|
43
|
+
<Text>Hidden Content</Text>
|
|
44
|
+
</InlineWidget>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(getByText('Hidden Content')).toBeTruthy();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should hide close button when visible is false', () => {
|
|
51
|
+
const { queryByText } = render(
|
|
52
|
+
<InlineWidget visible={false} height={300}>
|
|
53
|
+
<Text>Content</Text>
|
|
54
|
+
</InlineWidget>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
expect(queryByText('✕')).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should not throw when onClose is not provided and close is pressed', () => {
|
|
61
|
+
const { getByText } = render(
|
|
62
|
+
<InlineWidget visible={true} height={300}>
|
|
63
|
+
<Text>Content</Text>
|
|
64
|
+
</InlineWidget>
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(() => fireEvent.press(getByText('✕'))).not.toThrow();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should render multiple children', () => {
|
|
71
|
+
const { getByText } = render(
|
|
72
|
+
<InlineWidget visible={true} height={300}>
|
|
73
|
+
<Text>Child A</Text>
|
|
74
|
+
<Text>Child B</Text>
|
|
75
|
+
</InlineWidget>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
expect(getByText('Child A')).toBeTruthy();
|
|
79
|
+
expect(getByText('Child B')).toBeTruthy();
|
|
80
|
+
});
|
|
81
|
+
});
|