@solucx/react-native-solucx-widget 0.1.15 → 0.2.0
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/README.intern.md +513 -513
- package/README.md +285 -285
- package/package.json +50 -23
- package/src/SoluCXWidget.tsx +172 -119
- 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 +315 -0
- package/src/__tests__/e2e/widget-lifecycle.test.tsx +353 -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__/urlUtils.test.ts +56 -56
- package/src/__tests__/useDeviceInfoCollector.test.ts +109 -0
- package/src/__tests__/useWidgetState.test.ts +181 -189
- package/src/__tests__/widgetBootstrapService.test.ts +182 -0
- package/src/components/CloseButton.tsx +36 -36
- package/src/components/InlineWidget.tsx +36 -36
- package/src/components/ModalWidget.tsx +57 -59
- package/src/components/OverlayWidget.tsx +88 -88
- package/src/constants/Constants.ts +4 -0
- package/src/constants/webViewConstants.ts +15 -14
- package/src/hooks/index.ts +2 -2
- package/src/hooks/useDeviceInfoCollector.ts +67 -0
- package/src/hooks/useHeightAnimation.ts +22 -22
- package/src/hooks/useWidgetHeight.ts +38 -38
- package/src/hooks/useWidgetState.ts +101 -101
- package/src/index.ts +12 -8
- package/src/interfaces/WidgetCallbacks.ts +15 -0
- package/src/interfaces/WidgetData.ts +19 -19
- package/src/interfaces/WidgetOptions.ts +7 -7
- package/src/interfaces/WidgetResponse.ts +15 -15
- package/src/interfaces/WidgetSamplerLog.ts +5 -5
- package/src/interfaces/index.ts +25 -24
- package/src/services/ClientVersionCollector.ts +15 -0
- package/src/services/storage.ts +21 -21
- package/src/services/widgetBootstrapService.ts +67 -0
- package/src/services/widgetEventService.ts +110 -111
- package/src/services/widgetValidationService.ts +102 -86
- package/src/setupTests.js +43 -0
- package/src/styles/widgetStyles.ts +58 -58
- package/src/utils/urlUtils.ts +13 -13
package/src/SoluCXWidget.tsx
CHANGED
|
@@ -1,119 +1,172 @@
|
|
|
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 {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
resize,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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 { buildWidgetURL } from "./utils/urlUtils";
|
|
10
|
+
import { WEB_VIEW_MESSAGE_LISTENER } from "./constants/webViewConstants";
|
|
11
|
+
import { ModalWidget } from "./components/ModalWidget";
|
|
12
|
+
import { InlineWidget } from "./components/InlineWidget";
|
|
13
|
+
import { OverlayWidget } from "./components/OverlayWidget";
|
|
14
|
+
import { requestWidgetUrl } from "./services/widgetBootstrapService";
|
|
15
|
+
|
|
16
|
+
interface SoluCXWidgetProps {
|
|
17
|
+
soluCXKey: SoluCXKey;
|
|
18
|
+
type: WidgetType;
|
|
19
|
+
data: WidgetData;
|
|
20
|
+
options: WidgetOptions;
|
|
21
|
+
callbacks?: WidgetCallbacks;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const SoluCXWidget: React.FC<SoluCXWidgetProps> = ({ soluCXKey, type, data, options, callbacks }) => {
|
|
25
|
+
const webviewRef = useRef<WebView>(null);
|
|
26
|
+
const { width } = Dimensions.get("window");
|
|
27
|
+
|
|
28
|
+
const serializedData = JSON.stringify(data);
|
|
29
|
+
const normalizedData = useMemo(() => data, [serializedData]);
|
|
30
|
+
|
|
31
|
+
const { widgetHeight, isWidgetVisible, setIsWidgetVisible, loadSavedData, resize, open, close, userId } =
|
|
32
|
+
useWidgetState(normalizedData, options, type);
|
|
33
|
+
|
|
34
|
+
const eventService = useMemo(
|
|
35
|
+
() => new WidgetEventService(setIsWidgetVisible, resize, userId, callbacks),
|
|
36
|
+
[setIsWidgetVisible, resize],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const validationService = useMemo(() => new WidgetValidationService(userId), [userId]);
|
|
40
|
+
const isForm = Boolean(normalizedData.form_id);
|
|
41
|
+
const [widgetUri, setWidgetUri] = useState<string | null>(() =>
|
|
42
|
+
isForm ? buildWidgetURL(soluCXKey, normalizedData) : null,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
loadSavedData();
|
|
47
|
+
}, [loadSavedData]);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
let isActive = true;
|
|
51
|
+
|
|
52
|
+
const prepareWidgetURL = async () => {
|
|
53
|
+
if (isForm) {
|
|
54
|
+
setWidgetUri(buildWidgetURL(soluCXKey, normalizedData));
|
|
55
|
+
open(); // Show widget in form mode
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setWidgetUri(null);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const widgetUrl = await requestWidgetUrl(soluCXKey, normalizedData, userId);
|
|
63
|
+
if (!isActive || !widgetUrl) return;
|
|
64
|
+
|
|
65
|
+
const result = await validationService.shouldDisplayWidget(options);
|
|
66
|
+
if (!isActive) return;
|
|
67
|
+
|
|
68
|
+
if (!result.canDisplay) {
|
|
69
|
+
const blockReason = result?.blockReason;
|
|
70
|
+
callbacks?.onBlock?.(blockReason);
|
|
71
|
+
setIsWidgetVisible(false);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
callbacks?.onPreOpen?.(userId);
|
|
76
|
+
open();
|
|
77
|
+
setWidgetUri(widgetUrl);
|
|
78
|
+
callbacks?.onOpened?.(userId);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (isActive) {
|
|
81
|
+
callbacks?.onPingError?.(error);
|
|
82
|
+
setIsWidgetVisible(false);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
prepareWidgetURL();
|
|
88
|
+
|
|
89
|
+
return () => {
|
|
90
|
+
isActive = false;
|
|
91
|
+
};
|
|
92
|
+
}, [validationService, soluCXKey, normalizedData, isForm, setIsWidgetVisible, options, open, userId, callbacks]);
|
|
93
|
+
|
|
94
|
+
const handleWebViewMessage = useCallback(
|
|
95
|
+
async (message: string) => {
|
|
96
|
+
if (message && message.length > 0) {
|
|
97
|
+
try {
|
|
98
|
+
await eventService.handleMessage(message, isForm);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error("Error handling widget message:", error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
[eventService, isForm],
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const handleWebViewLoad = useCallback(() => {
|
|
108
|
+
webviewRef.current?.injectJavaScript(WEB_VIEW_MESSAGE_LISTENER);
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
const handleClose = useCallback(() => {
|
|
112
|
+
if (type === "inline" || type === "modal") {
|
|
113
|
+
close();
|
|
114
|
+
}
|
|
115
|
+
setIsWidgetVisible(false);
|
|
116
|
+
}, [setIsWidgetVisible, close, type]);
|
|
117
|
+
|
|
118
|
+
const webViewStyle = [{ height: widgetHeight }, { width }];
|
|
119
|
+
|
|
120
|
+
if (!widgetUri) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (type === "modal") {
|
|
125
|
+
return (
|
|
126
|
+
<ModalWidget visible={isWidgetVisible} height={widgetHeight} onClose={handleClose}>
|
|
127
|
+
<WebView
|
|
128
|
+
ref={webviewRef}
|
|
129
|
+
style={webViewStyle}
|
|
130
|
+
source={{ uri: widgetUri }}
|
|
131
|
+
onLoadEnd={handleWebViewLoad}
|
|
132
|
+
onMessage={(event) => handleWebViewMessage(event.nativeEvent.data)}
|
|
133
|
+
originWhitelist={["*"]}
|
|
134
|
+
/>
|
|
135
|
+
</ModalWidget>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (type === "inline") {
|
|
140
|
+
return (
|
|
141
|
+
<InlineWidget visible={isWidgetVisible} height={widgetHeight} onClose={handleClose}>
|
|
142
|
+
<WebView
|
|
143
|
+
ref={webviewRef}
|
|
144
|
+
style={webViewStyle}
|
|
145
|
+
source={{ uri: widgetUri }}
|
|
146
|
+
onLoadEnd={handleWebViewLoad}
|
|
147
|
+
onMessage={(event) => handleWebViewMessage(event.nativeEvent.data)}
|
|
148
|
+
originWhitelist={["*"]}
|
|
149
|
+
/>
|
|
150
|
+
</InlineWidget>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<OverlayWidget
|
|
156
|
+
visible={isWidgetVisible}
|
|
157
|
+
width={width}
|
|
158
|
+
height={widgetHeight}
|
|
159
|
+
position={type}
|
|
160
|
+
onClose={handleClose}
|
|
161
|
+
>
|
|
162
|
+
<WebView
|
|
163
|
+
ref={webviewRef}
|
|
164
|
+
style={webViewStyle}
|
|
165
|
+
source={{ uri: widgetUri }}
|
|
166
|
+
onLoadEnd={handleWebViewLoad}
|
|
167
|
+
onMessage={(event) => handleWebViewMessage(event.nativeEvent.data)}
|
|
168
|
+
originWhitelist={["*"]}
|
|
169
|
+
/>
|
|
170
|
+
</OverlayWidget>
|
|
171
|
+
);
|
|
172
|
+
};
|
|
@@ -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
|
+
});
|