@wutiange/log-listener-plugin 1.3.1 → 2.0.1-alpha.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/README.md +155 -2
- package/dist/src/HTTPInterceptor.d.ts +1 -0
- package/dist/src/HTTPInterceptor.js +12 -9
- package/dist/src/HTTPInterceptor.js.map +1 -1
- package/dist/src/Server.d.ts +13 -6
- package/dist/src/Server.js +119 -41
- package/dist/src/Server.js.map +1 -1
- package/dist/src/__mocks__/react-native/Libraries/Blob/FileReader.js +0 -1
- package/dist/src/__mocks__/react-native/Libraries/Blob/FileReader.js.map +1 -1
- package/dist/src/__mocks__/react-native/Libraries/Network/XHRInterceptor.js +0 -1
- package/dist/src/__mocks__/react-native/Libraries/Network/XHRInterceptor.js.map +1 -1
- package/dist/src/__tests__/Server.test.js +89 -114
- package/dist/src/__tests__/Server.test.js.map +1 -1
- package/dist/src/common.d.ts +19 -10
- package/dist/src/common.js +63 -4
- package/dist/src/common.js.map +1 -1
- package/dist/src/logPlugin.d.ts +12 -9
- package/dist/src/logPlugin.js +87 -82
- package/dist/src/logPlugin.js.map +1 -1
- package/dist/src/logger.d.ts +6 -0
- package/dist/src/logger.js +16 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/utils.js +12 -7
- package/dist/src/utils.js.map +1 -1
- package/index.ts +3 -0
- package/package.json +18 -12
- package/src/HTTPInterceptor.ts +339 -0
- package/src/Server.ts +166 -0
- package/src/__mocks__/react-native/Libraries/Blob/FileReader.js +45 -0
- package/src/__mocks__/react-native/Libraries/Network/XHRInterceptor.js +39 -0
- package/src/__tests__/HTTPInterceptor.test.ts +322 -0
- package/src/__tests__/Server.test.ts +175 -0
- package/src/__tests__/utils.test.ts +113 -0
- package/src/common.ts +70 -0
- package/src/logPlugin.ts +224 -0
- package/src/logger.ts +15 -0
- package/src/utils.ts +112 -0
- package/tsconfig.json +27 -0
@@ -0,0 +1,322 @@
|
|
1
|
+
import { httpInterceptor } from '../HTTPInterceptor';
|
2
|
+
import XHRInterceptor from 'react-native/Libraries/Network/XHRInterceptor';
|
3
|
+
import { formDataToString } from '../utils';
|
4
|
+
|
5
|
+
// Mock dependencies
|
6
|
+
jest.mock('buffer', () => ({
|
7
|
+
Blob: jest.fn(),
|
8
|
+
}));
|
9
|
+
jest.mock('../utils', () => ({
|
10
|
+
createClassWithErrorHandling: jest.fn(Class => Class),
|
11
|
+
formDataToString: jest.fn(),
|
12
|
+
}));
|
13
|
+
|
14
|
+
// 在文件顶部添加这个模拟
|
15
|
+
class MockFormData {
|
16
|
+
private data: Record<string, string> = {};
|
17
|
+
|
18
|
+
append(key: string, value: string) {
|
19
|
+
this.data[key] = value;
|
20
|
+
}
|
21
|
+
|
22
|
+
get(key: string) {
|
23
|
+
return this.data[key];
|
24
|
+
}
|
25
|
+
|
26
|
+
// 可以根据需要添加更多方法
|
27
|
+
}
|
28
|
+
|
29
|
+
class MockBlob {
|
30
|
+
private content: string;
|
31
|
+
type: any
|
32
|
+
constructor(parts: any, options: any = {}) {
|
33
|
+
this.content = parts ? parts.join('') : '';
|
34
|
+
this.type = options.type || '';
|
35
|
+
}
|
36
|
+
|
37
|
+
text(): Promise<string> {
|
38
|
+
return Promise.resolve(this.content);
|
39
|
+
}
|
40
|
+
|
41
|
+
arrayBuffer() {
|
42
|
+
return Promise.resolve(new ArrayBuffer(0));
|
43
|
+
}
|
44
|
+
}
|
45
|
+
// 全局声明,以避免 TypeScript 错误
|
46
|
+
declare global {
|
47
|
+
// @ts-ignore
|
48
|
+
var FormData: typeof MockFormData;
|
49
|
+
}
|
50
|
+
|
51
|
+
// 在测试套件开始前设置全局 FormData
|
52
|
+
beforeAll(() => {
|
53
|
+
// @ts-ignore
|
54
|
+
global.FormData = MockFormData;
|
55
|
+
});
|
56
|
+
|
57
|
+
// 在测试套件结束后清理
|
58
|
+
afterAll(() => {
|
59
|
+
// @ts-ignore
|
60
|
+
delete global.FormData;
|
61
|
+
});
|
62
|
+
|
63
|
+
describe('HTTPInterceptor', () => {
|
64
|
+
beforeEach(() => {
|
65
|
+
jest.clearAllMocks();
|
66
|
+
console.warn = jest.fn();
|
67
|
+
httpInterceptor.reset();
|
68
|
+
(XHRInterceptor.isInterceptorEnabled as jest.Mock).mockReturnValue(false);
|
69
|
+
});
|
70
|
+
|
71
|
+
describe('enable', () => {
|
72
|
+
it('should enable interception', () => {
|
73
|
+
httpInterceptor.enable();
|
74
|
+
expect(XHRInterceptor.enableInterception).toHaveBeenCalled();
|
75
|
+
});
|
76
|
+
|
77
|
+
it('should not enable if already enabled', () => {
|
78
|
+
httpInterceptor.enable();
|
79
|
+
httpInterceptor.enable();
|
80
|
+
expect(XHRInterceptor.enableInterception).toHaveBeenCalledTimes(1);
|
81
|
+
});
|
82
|
+
|
83
|
+
it('should handle ignored hosts', () => {
|
84
|
+
httpInterceptor.enable({ ignoredHosts: ['example.com'] });
|
85
|
+
expect(XHRInterceptor.enableInterception).toHaveBeenCalled();
|
86
|
+
});
|
87
|
+
|
88
|
+
it('should handle ignored patterns', () => {
|
89
|
+
httpInterceptor.enable({ ignoredPatterns: [/^GET https:\/\/test\.com/] });
|
90
|
+
expect(XHRInterceptor.enableInterception).toHaveBeenCalled();
|
91
|
+
});
|
92
|
+
|
93
|
+
it('should handle ignored URLs', () => {
|
94
|
+
httpInterceptor.enable({ ignoredUrls: ['https://example.com/api'] });
|
95
|
+
expect(XHRInterceptor.enableInterception).toHaveBeenCalled();
|
96
|
+
});
|
97
|
+
|
98
|
+
it('should warn if another interceptor is running', () => {
|
99
|
+
(XHRInterceptor.isInterceptorEnabled as jest.Mock).mockReturnValue(true);
|
100
|
+
console.warn = jest.fn();
|
101
|
+
httpInterceptor.enable();
|
102
|
+
expect(console.warn).toHaveBeenCalled();
|
103
|
+
});
|
104
|
+
|
105
|
+
it('should force enable if specified', () => {
|
106
|
+
(XHRInterceptor.isInterceptorEnabled as jest.Mock).mockReturnValue(true);
|
107
|
+
httpInterceptor.enable({ forceEnable: true });
|
108
|
+
expect(XHRInterceptor.enableInterception).toHaveBeenCalled();
|
109
|
+
});
|
110
|
+
});
|
111
|
+
|
112
|
+
describe('disable', () => {
|
113
|
+
it('should disable interception', () => {
|
114
|
+
httpInterceptor.enable();
|
115
|
+
httpInterceptor.disable();
|
116
|
+
expect(XHRInterceptor.disableInterception).toHaveBeenCalled();
|
117
|
+
});
|
118
|
+
|
119
|
+
it('should not disable if not enabled', () => {
|
120
|
+
httpInterceptor.disable();
|
121
|
+
expect(XHRInterceptor.disableInterception).not.toHaveBeenCalled();
|
122
|
+
});
|
123
|
+
});
|
124
|
+
|
125
|
+
describe('listeners', () => {
|
126
|
+
it('should add and remove listeners', () => {
|
127
|
+
const listener = jest.fn();
|
128
|
+
const removeListener = httpInterceptor.addListener('open', listener);
|
129
|
+
expect(httpInterceptor['userListeners'].length).toBe(1);
|
130
|
+
removeListener();
|
131
|
+
expect(httpInterceptor['userListeners'].length).toBe(0);
|
132
|
+
});
|
133
|
+
|
134
|
+
it('should not add duplicate listeners', () => {
|
135
|
+
const listener = jest.fn();
|
136
|
+
httpInterceptor.addListener('open', listener);
|
137
|
+
httpInterceptor.addListener('open', listener);
|
138
|
+
expect(httpInterceptor['userListeners'].length).toBe(1);
|
139
|
+
});
|
140
|
+
|
141
|
+
it('should remove specific listener', () => {
|
142
|
+
const listener1 = jest.fn();
|
143
|
+
const listener2 = jest.fn();
|
144
|
+
httpInterceptor.addListener('open', listener1);
|
145
|
+
httpInterceptor.addListener('open', listener2);
|
146
|
+
httpInterceptor.removeListener('open', listener1);
|
147
|
+
expect(httpInterceptor['userListeners'].length).toBe(1);
|
148
|
+
expect(httpInterceptor['userListeners'][0][1]).toBe(listener2);
|
149
|
+
});
|
150
|
+
|
151
|
+
it('should remove all listeners', () => {
|
152
|
+
httpInterceptor.addListener('open', jest.fn());
|
153
|
+
httpInterceptor.addListener('send', jest.fn());
|
154
|
+
httpInterceptor.removeAllListener();
|
155
|
+
expect(httpInterceptor['userListeners'].length).toBe(0);
|
156
|
+
});
|
157
|
+
});
|
158
|
+
|
159
|
+
describe('request handling', () => {
|
160
|
+
let openCallback: Function;
|
161
|
+
let requestHeaderCallback: Function;
|
162
|
+
let headerReceivedCallback: Function;
|
163
|
+
let sendCallback: Function;
|
164
|
+
let responseCallback: Function;
|
165
|
+
|
166
|
+
beforeEach(() => {
|
167
|
+
httpInterceptor.enable();
|
168
|
+
openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock.calls[0][0];
|
169
|
+
requestHeaderCallback = (XHRInterceptor.setRequestHeaderCallback as jest.Mock).mock.calls[0][0];
|
170
|
+
headerReceivedCallback = (XHRInterceptor.setHeaderReceivedCallback as jest.Mock).mock.calls[0][0];
|
171
|
+
sendCallback = (XHRInterceptor.setSendCallback as jest.Mock).mock.calls[0][0];
|
172
|
+
responseCallback = (XHRInterceptor.setResponseCallback as jest.Mock).mock.calls[0][0];
|
173
|
+
});
|
174
|
+
|
175
|
+
it('should handle open event', () => {
|
176
|
+
const listener = jest.fn();
|
177
|
+
httpInterceptor.addListener('open', listener);
|
178
|
+
const xhr = {}
|
179
|
+
openCallback('GET', 'https://example.com', xhr);
|
180
|
+
expect(listener).toHaveBeenCalledWith(expect.objectContaining({
|
181
|
+
method: 'GET',
|
182
|
+
url: 'https://example.com',
|
183
|
+
}));
|
184
|
+
});
|
185
|
+
|
186
|
+
it('should handle request header event', () => {
|
187
|
+
const listener = jest.fn();
|
188
|
+
httpInterceptor.addListener('requestHeader', listener);
|
189
|
+
const xhr = {}
|
190
|
+
openCallback('GET', 'https://example.com', xhr);
|
191
|
+
requestHeaderCallback('Content-Type', 'application/json', xhr);
|
192
|
+
expect(listener).toHaveBeenCalledWith(expect.objectContaining({
|
193
|
+
requestHeaders: { 'Content-Type': 'application/json' },
|
194
|
+
}));
|
195
|
+
});
|
196
|
+
|
197
|
+
it('should handle header received event', () => {
|
198
|
+
const listener = jest.fn();
|
199
|
+
httpInterceptor.addListener('headerReceived', listener);
|
200
|
+
const xhr: {[key in string]: any} = {}
|
201
|
+
openCallback('GET', 'https://example.com', xhr);
|
202
|
+
xhr.responseHeaders = { 'Content-Type': 'application/json' }
|
203
|
+
headerReceivedCallback('application/json', 100, {}, xhr);
|
204
|
+
expect(listener).toHaveBeenCalledWith(expect.objectContaining({
|
205
|
+
responseContentType: 'application/json',
|
206
|
+
responseSize: 100,
|
207
|
+
responseHeaders: { 'Content-Type': 'application/json' },
|
208
|
+
}));
|
209
|
+
});
|
210
|
+
|
211
|
+
it('should handle send event with JSON data', () => {
|
212
|
+
const listener = jest.fn();
|
213
|
+
httpInterceptor.addListener('send', listener);
|
214
|
+
const xhr = {}
|
215
|
+
openCallback('POST', 'https://example.com', xhr);
|
216
|
+
sendCallback(JSON.stringify({ key: 'value' }), xhr);
|
217
|
+
expect(listener).toHaveBeenCalledWith(expect.objectContaining({
|
218
|
+
requestData: { key: 'value' },
|
219
|
+
}));
|
220
|
+
});
|
221
|
+
|
222
|
+
it('should handle send event with FormData', () => {
|
223
|
+
const listener = jest.fn();
|
224
|
+
httpInterceptor.addListener('send', listener);
|
225
|
+
const xhr = {}
|
226
|
+
openCallback('POST', 'https://example.com', xhr);
|
227
|
+
const formData = new FormData();
|
228
|
+
formData.append('key', 'value');
|
229
|
+
(formDataToString as jest.Mock).mockReturnValue('key=value');
|
230
|
+
sendCallback(formData, xhr);
|
231
|
+
expect(listener).toHaveBeenCalledWith(expect.objectContaining({
|
232
|
+
requestData: 'key=value',
|
233
|
+
}));
|
234
|
+
});
|
235
|
+
|
236
|
+
it('should handle response event', async () => {
|
237
|
+
const listener = jest.fn();
|
238
|
+
httpInterceptor.addListener('response', listener);
|
239
|
+
const xhr = {}
|
240
|
+
openCallback('GET', 'https://example.com', xhr);
|
241
|
+
await responseCallback(200, 1000, { data: 'response' }, 'https://example.com', 'json', xhr);
|
242
|
+
expect(listener).toHaveBeenCalledWith(expect.objectContaining({
|
243
|
+
status: 200,
|
244
|
+
timeout: 1000,
|
245
|
+
responseData: { data: 'response' },
|
246
|
+
responseURL: 'https://example.com',
|
247
|
+
responseType: 'json',
|
248
|
+
}));
|
249
|
+
});
|
250
|
+
|
251
|
+
it('should handle response event with blob data', async () => {
|
252
|
+
const listener = jest.fn();
|
253
|
+
httpInterceptor.addListener('response', listener);
|
254
|
+
const xhr = {}
|
255
|
+
openCallback('GET', 'https://example.com', xhr);
|
256
|
+
const mockBlob = new MockBlob(['blob content']);
|
257
|
+
await responseCallback(200, 1000, mockBlob, 'https://example.com', 'blob', xhr);
|
258
|
+
expect(listener).toHaveBeenCalledWith(expect.objectContaining({
|
259
|
+
responseData: 'blob content',
|
260
|
+
}));
|
261
|
+
});
|
262
|
+
});
|
263
|
+
|
264
|
+
describe('error handling', () => {
|
265
|
+
beforeEach(() => {
|
266
|
+
httpInterceptor.enable();
|
267
|
+
})
|
268
|
+
it('should handle errors in listeners', async () => {
|
269
|
+
const errorListener = jest.fn(() => {
|
270
|
+
throw new Error('Listener error');
|
271
|
+
});
|
272
|
+
httpInterceptor.addListener('open', errorListener);
|
273
|
+
console.warn = jest.fn();
|
274
|
+
const xhr = {}
|
275
|
+
const openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock.calls[0][0];
|
276
|
+
openCallback('GET', 'https://example.com', xhr);
|
277
|
+
|
278
|
+
expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('Listener error'));
|
279
|
+
});
|
280
|
+
});
|
281
|
+
|
282
|
+
describe('ignored requests', () => {
|
283
|
+
it('should ignore requests to ignored hosts', () => {
|
284
|
+
httpInterceptor.enable({ ignoredHosts: ['ignored.com'] });
|
285
|
+
const openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock.calls[0][0];
|
286
|
+
const listener = jest.fn();
|
287
|
+
httpInterceptor.addListener('open', listener);
|
288
|
+
|
289
|
+
openCallback('GET', 'https://ignored.com', { uniqueId: '123' });
|
290
|
+
expect(listener).not.toHaveBeenCalled();
|
291
|
+
|
292
|
+
openCallback('GET', 'https://example.com', { uniqueId: '124' });
|
293
|
+
expect(listener).toHaveBeenCalled();
|
294
|
+
});
|
295
|
+
|
296
|
+
it('should ignore requests to ignored URLs', () => {
|
297
|
+
httpInterceptor.enable({ ignoredUrls: ['https://example.com/ignored'] });
|
298
|
+
const openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock.calls[0][0];
|
299
|
+
const listener = jest.fn();
|
300
|
+
httpInterceptor.addListener('open', listener);
|
301
|
+
|
302
|
+
openCallback('GET', 'https://example.com/ignored', { uniqueId: '123' });
|
303
|
+
expect(listener).not.toHaveBeenCalled();
|
304
|
+
|
305
|
+
openCallback('GET', 'https://example.com/api', { uniqueId: '124' });
|
306
|
+
expect(listener).toHaveBeenCalled();
|
307
|
+
});
|
308
|
+
|
309
|
+
it('should ignore requests matching ignored patterns', () => {
|
310
|
+
httpInterceptor.enable({ ignoredPatterns: [/^GET https:\/\/test\.com/] });
|
311
|
+
const openCallback = (XHRInterceptor.setOpenCallback as jest.Mock).mock.calls[0][0];
|
312
|
+
const listener = jest.fn();
|
313
|
+
httpInterceptor.addListener('open', listener);
|
314
|
+
|
315
|
+
openCallback('GET', 'https://test.com/api', { uniqueId: '123' });
|
316
|
+
expect(listener).not.toHaveBeenCalled();
|
317
|
+
|
318
|
+
openCallback('POST', 'https://test.com/api', { uniqueId: '124' });
|
319
|
+
expect(listener).toHaveBeenCalled();
|
320
|
+
});
|
321
|
+
});
|
322
|
+
});
|
@@ -0,0 +1,175 @@
|
|
1
|
+
import Server from '../Server';
|
2
|
+
import { sleep } from '../utils';
|
3
|
+
|
4
|
+
// Mock dependencies
|
5
|
+
jest.mock('../utils', () => {
|
6
|
+
const actual = jest.requireActual('../utils');
|
7
|
+
return {
|
8
|
+
...actual, // 保留所有真实实现
|
9
|
+
sleep: jest.fn() // 只模拟 sleep 函数
|
10
|
+
};
|
11
|
+
});
|
12
|
+
|
13
|
+
|
14
|
+
// Mock require for react-native-zeroconf
|
15
|
+
jest.mock('react-native-zeroconf', () => undefined, { virtual: true });
|
16
|
+
|
17
|
+
// Mock fetch
|
18
|
+
global.fetch = jest.fn();
|
19
|
+
|
20
|
+
describe('Server', () => {
|
21
|
+
let server: Server;
|
22
|
+
|
23
|
+
beforeEach(() => {
|
24
|
+
// Clear all mocks before each test
|
25
|
+
jest.clearAllMocks();
|
26
|
+
(global.fetch as jest.Mock).mockReset();
|
27
|
+
});
|
28
|
+
|
29
|
+
describe('Constructor and URL Management', () => {
|
30
|
+
it('should initialize with default values', () => {
|
31
|
+
server = new Server();
|
32
|
+
expect(server.getUrls()).toEqual([]);
|
33
|
+
});
|
34
|
+
|
35
|
+
it('should initialize with custom URL', () => {
|
36
|
+
server = new Server('localhost:8080');
|
37
|
+
expect(server.getUrls()).toEqual(['http://localhost:8080']);
|
38
|
+
});
|
39
|
+
|
40
|
+
it('should handle URLs with and without http prefix', () => {
|
41
|
+
server = new Server();
|
42
|
+
server.updateUrl('localhost:8080');
|
43
|
+
expect(server.getUrls()).toEqual(['http://localhost:8080']);
|
44
|
+
|
45
|
+
server.updateUrl('http://localhost:8080');
|
46
|
+
expect(server.getUrls()).toEqual(['http://localhost:8080']);
|
47
|
+
});
|
48
|
+
});
|
49
|
+
|
50
|
+
describe('ZeroConf Handling', () => {
|
51
|
+
it('should handle case when zeroconf is not available', () => {
|
52
|
+
server = new Server();
|
53
|
+
const mockListener = jest.fn();
|
54
|
+
server.addUrlsListener(mockListener);
|
55
|
+
|
56
|
+
// Since Zeroconf is not available, the listener should not be called
|
57
|
+
expect(mockListener).not.toHaveBeenCalled();
|
58
|
+
});
|
59
|
+
|
60
|
+
// Test with mock Zeroconf implementation
|
61
|
+
it('should handle case when zeroconf is available', () => {
|
62
|
+
// Temporarily mock require to return a mock Zeroconf implementation
|
63
|
+
const mockZeroconfInstance = {
|
64
|
+
on: jest.fn(),
|
65
|
+
scan: jest.fn()
|
66
|
+
};
|
67
|
+
|
68
|
+
jest.doMock('react-native-zeroconf', () => ({
|
69
|
+
__esModule: true,
|
70
|
+
default: jest.fn(() => mockZeroconfInstance)
|
71
|
+
}), { virtual: true });
|
72
|
+
|
73
|
+
server = new Server();
|
74
|
+
const mockListener = jest.fn();
|
75
|
+
server.addUrlsListener(mockListener);
|
76
|
+
|
77
|
+
// Verify that Zeroconf methods were not called since module is mocked as undefined
|
78
|
+
expect(mockListener).not.toHaveBeenCalled();
|
79
|
+
});
|
80
|
+
});
|
81
|
+
|
82
|
+
describe('Data Sending', () => {
|
83
|
+
beforeEach(() => {
|
84
|
+
server = new Server('localhost:8080');
|
85
|
+
(global.fetch as jest.Mock).mockImplementation(() =>
|
86
|
+
Promise.resolve({ ok: true })
|
87
|
+
);
|
88
|
+
});
|
89
|
+
|
90
|
+
it('should send log data', async () => {
|
91
|
+
const testData = { message: 'test log' };
|
92
|
+
await server.log(testData);
|
93
|
+
|
94
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
95
|
+
'http://localhost:8080/log',
|
96
|
+
expect.objectContaining({
|
97
|
+
method: 'POST',
|
98
|
+
headers: {
|
99
|
+
'Content-Type': 'application/json;charset=utf-8'
|
100
|
+
},
|
101
|
+
body: expect.any(String)
|
102
|
+
})
|
103
|
+
);
|
104
|
+
});
|
105
|
+
|
106
|
+
it('should send network data', async () => {
|
107
|
+
const testData = { url: 'test.com' };
|
108
|
+
await server.network(testData);
|
109
|
+
|
110
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
111
|
+
'http://localhost:8080/network',
|
112
|
+
expect.objectContaining({
|
113
|
+
method: 'POST',
|
114
|
+
headers: {
|
115
|
+
'Content-Type': 'application/json;charset=utf-8'
|
116
|
+
},
|
117
|
+
body: expect.any(String)
|
118
|
+
})
|
119
|
+
);
|
120
|
+
});
|
121
|
+
|
122
|
+
it('should handle timeout', async () => {
|
123
|
+
server.updateTimeout(100);
|
124
|
+
(sleep as jest.Mock).mockImplementation(() => Promise.reject(new Error('Timeout')));
|
125
|
+
|
126
|
+
const testData = { message: 'test' };
|
127
|
+
await server.log(testData);
|
128
|
+
|
129
|
+
expect(global.fetch).toHaveBeenCalled();
|
130
|
+
expect(sleep).toHaveBeenCalledWith(100, true);
|
131
|
+
});
|
132
|
+
|
133
|
+
it('should handle errors and send error data', async () => {
|
134
|
+
(global.fetch as jest.Mock).mockImplementation(() =>
|
135
|
+
Promise.reject(new Error('Network error'))
|
136
|
+
);
|
137
|
+
|
138
|
+
const testData = { message: 'test' };
|
139
|
+
await server.log(testData);
|
140
|
+
|
141
|
+
// 修改这里的断言来匹配实际的错误格式
|
142
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
143
|
+
expect.any(String),
|
144
|
+
expect.objectContaining({
|
145
|
+
body: expect.stringContaining('log-plugin-internal-error')
|
146
|
+
})
|
147
|
+
);
|
148
|
+
|
149
|
+
// 如果需要更精确的检查,可以添加以下断言
|
150
|
+
const lastCall = (global.fetch as jest.Mock).mock.calls[(global.fetch as jest.Mock).mock.calls.length - 1];
|
151
|
+
const body = JSON.parse(lastCall[1].body);
|
152
|
+
expect(body).toMatchObject({
|
153
|
+
tag: 'log-plugin-internal-error',
|
154
|
+
level: 'error'
|
155
|
+
});
|
156
|
+
});
|
157
|
+
});
|
158
|
+
|
159
|
+
describe('Base Data Management', () => {
|
160
|
+
it('should update base data', async () => {
|
161
|
+
server = new Server('localhost:8080');
|
162
|
+
const baseData = { userId: '123' };
|
163
|
+
server.updateBaseData(baseData);
|
164
|
+
|
165
|
+
await server.log({ message: 'test' });
|
166
|
+
|
167
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
168
|
+
expect.any(String),
|
169
|
+
expect.objectContaining({
|
170
|
+
body: expect.stringContaining('"userId":"123"')
|
171
|
+
})
|
172
|
+
);
|
173
|
+
});
|
174
|
+
});
|
175
|
+
});
|
@@ -0,0 +1,113 @@
|
|
1
|
+
import { createClassWithErrorHandling, hasPort } from '../utils';
|
2
|
+
|
3
|
+
describe('createClassWithErrorHandling', () => {
|
4
|
+
class TestClass {
|
5
|
+
normalMethod(): string {
|
6
|
+
return 'normal';
|
7
|
+
}
|
8
|
+
|
9
|
+
errorMethod(): void {
|
10
|
+
throw new Error('Test error');
|
11
|
+
}
|
12
|
+
|
13
|
+
async asyncMethod(): Promise<string> {
|
14
|
+
return 'async';
|
15
|
+
}
|
16
|
+
|
17
|
+
async asyncErrorMethod(): Promise<void> {
|
18
|
+
throw new Error('Async test error');
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
let consoleErrorSpy: jest.SpyInstance;
|
23
|
+
|
24
|
+
beforeEach(() => {
|
25
|
+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
26
|
+
});
|
27
|
+
|
28
|
+
afterEach(() => {
|
29
|
+
consoleErrorSpy.mockRestore();
|
30
|
+
});
|
31
|
+
|
32
|
+
test('should not interfere with normal methods', () => {
|
33
|
+
const EnhancedClass = createClassWithErrorHandling(TestClass);
|
34
|
+
const instance = new EnhancedClass();
|
35
|
+
expect(instance.normalMethod()).toBe('normal');
|
36
|
+
});
|
37
|
+
|
38
|
+
test('should catch and log errors from methods', () => {
|
39
|
+
const EnhancedClass = createClassWithErrorHandling(TestClass);
|
40
|
+
const instance = new EnhancedClass();
|
41
|
+
expect(() => instance.errorMethod()).toThrow('Test error');
|
42
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Error in errorMethod:', expect.any(Error));
|
43
|
+
});
|
44
|
+
|
45
|
+
test('should not interfere with async methods that resolve', async () => {
|
46
|
+
const EnhancedClass = createClassWithErrorHandling(TestClass);
|
47
|
+
const instance = new EnhancedClass();
|
48
|
+
await expect(instance.asyncMethod()).resolves.toBe('async');
|
49
|
+
});
|
50
|
+
|
51
|
+
test('should catch and log errors from async methods that reject', async () => {
|
52
|
+
const EnhancedClass = createClassWithErrorHandling(TestClass);
|
53
|
+
const instance = new EnhancedClass();
|
54
|
+
await expect(instance.asyncErrorMethod()).rejects.toThrow('Async test error');
|
55
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Error in asyncErrorMethod:', expect.any(Error));
|
56
|
+
});
|
57
|
+
|
58
|
+
test('should handle methods added after instantiation', () => {
|
59
|
+
const EnhancedClass = createClassWithErrorHandling(TestClass);
|
60
|
+
const instance = new EnhancedClass();
|
61
|
+
(instance as any).dynamicMethod = function(): void {
|
62
|
+
throw new Error('Dynamic method error');
|
63
|
+
};
|
64
|
+
expect(() => (instance as any).dynamicMethod()).toThrow('Dynamic method error');
|
65
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('Error in dynamicMethod:', expect.any(Error));
|
66
|
+
});
|
67
|
+
});
|
68
|
+
|
69
|
+
|
70
|
+
describe('hasPort function', () => {
|
71
|
+
test('should return true for URLs with explicit ports', () => {
|
72
|
+
expect(hasPort('http://example.com:8080')).toBe(true);
|
73
|
+
expect(hasPort('ftp://example.com:210')).toBe(true);
|
74
|
+
});
|
75
|
+
|
76
|
+
test('should return false for URLs without explicit ports', () => {
|
77
|
+
expect(hasPort('http://example.com')).toBe(false);
|
78
|
+
expect(hasPort('https://example.com')).toBe(false);
|
79
|
+
expect(hasPort('ftp://example.com')).toBe(false);
|
80
|
+
});
|
81
|
+
|
82
|
+
test('should return false for invalid URLs', () => {
|
83
|
+
expect(hasPort('not a url')).toBe(false);
|
84
|
+
expect(hasPort('http:/example.com')).toBe(false);
|
85
|
+
expect(hasPort('example.com:8080')).toBe(false);
|
86
|
+
});
|
87
|
+
|
88
|
+
test('should return false for empty input', () => {
|
89
|
+
expect(hasPort('')).toBe(false);
|
90
|
+
});
|
91
|
+
|
92
|
+
test('should return false for non-string input', () => {
|
93
|
+
expect(hasPort(null as any)).toBe(false);
|
94
|
+
expect(hasPort(undefined as any)).toBe(false);
|
95
|
+
expect(hasPort(123 as any)).toBe(false);
|
96
|
+
expect(hasPort({} as any)).toBe(false);
|
97
|
+
});
|
98
|
+
|
99
|
+
test('should handle URLs with default ports correctly', () => {
|
100
|
+
expect(hasPort('http://example.com:80')).toBe(false);
|
101
|
+
expect(hasPort('https://example.com:443')).toBe(false);
|
102
|
+
});
|
103
|
+
|
104
|
+
test('should handle URLs with IPv6 addresses', () => {
|
105
|
+
expect(hasPort('http://[2001:db8::1]:8080')).toBe(true);
|
106
|
+
expect(hasPort('https://[2001:db8::1]')).toBe(false);
|
107
|
+
});
|
108
|
+
|
109
|
+
test('should handle URLs with userinfo', () => {
|
110
|
+
expect(hasPort('http://user:pass@example.com:8080')).toBe(true);
|
111
|
+
expect(hasPort('http://user:pass@example.com')).toBe(false);
|
112
|
+
});
|
113
|
+
});
|
package/src/common.ts
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
import logger from "./logger";
|
2
|
+
|
3
|
+
export const URLS_KEY = 'log-listener-plugin-urls$$key'
|
4
|
+
export const DEFAULT_TIMEOUT = 3000
|
5
|
+
export const LOG_KEY = '[@wutiange/log-listener-plugin 日志]'
|
6
|
+
export enum Level {
|
7
|
+
LOG = 'log',
|
8
|
+
WARN = 'warn',
|
9
|
+
ERROR = 'error',
|
10
|
+
}
|
11
|
+
|
12
|
+
export enum Tag {
|
13
|
+
LOG_PLUGIN_INTERNAL_ERROR = 'log-plugin-internal-error',
|
14
|
+
DEFAULT = 'default',
|
15
|
+
}
|
16
|
+
|
17
|
+
const getDefaultDeviceINfo = () => {
|
18
|
+
try {
|
19
|
+
const {Platform} = require('react-native')
|
20
|
+
return {
|
21
|
+
SystemName: Platform.OS,
|
22
|
+
Version: Platform.Version,
|
23
|
+
...Platform.constants
|
24
|
+
}
|
25
|
+
} catch (error) {
|
26
|
+
logger.warn(LOG_KEY, '这个插件只能在 react-native 中使用')
|
27
|
+
return {}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
export const getBaseData = (): Record<string, string> => {
|
32
|
+
|
33
|
+
try {
|
34
|
+
const DeviceInfo = require("react-native-device-info")?.default;
|
35
|
+
return {
|
36
|
+
Brand: DeviceInfo.getBrand(),
|
37
|
+
Model: DeviceInfo.getModel(),
|
38
|
+
AppVersion: DeviceInfo.getVersion(),
|
39
|
+
Carrier: DeviceInfo.getCarrierSync(),
|
40
|
+
Manufacturer: DeviceInfo.getManufacturerSync(),
|
41
|
+
SystemName: DeviceInfo.getSystemName(),
|
42
|
+
...getDefaultDeviceINfo()
|
43
|
+
};
|
44
|
+
} catch (error) {
|
45
|
+
return getDefaultDeviceINfo()
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
export const getDefaultStorage = (): Storage => {
|
50
|
+
try {
|
51
|
+
const AsyncStorage = require("@react-native-async-storage/async-storage")?.default;
|
52
|
+
return AsyncStorage;
|
53
|
+
} catch (error) {
|
54
|
+
return null;
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
|
59
|
+
export const getErrMsg = (error: any) => {
|
60
|
+
return {
|
61
|
+
message: [
|
62
|
+
`${
|
63
|
+
error?.message ?? error
|
64
|
+
}--->这是@wutiange/log-listener-plugin内部错误,请提issue反馈,issue地址:https://github.com/wutiange/log-listener-plugin/issues`,
|
65
|
+
],
|
66
|
+
tag: Tag.LOG_PLUGIN_INTERNAL_ERROR,
|
67
|
+
level: Level.ERROR,
|
68
|
+
createTime: Date.now(),
|
69
|
+
}
|
70
|
+
}
|