@elizaos/plugin-ngrok 2.0.0-beta.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 +325 -0
- package/dist/__tests__/NgrokTestSuite.d.ts +6 -0
- package/dist/__tests__/NgrokTestSuite.d.ts.map +1 -0
- package/dist/__tests__/NgrokTestSuite.js +92 -0
- package/dist/__tests__/NgrokTestSuite.js.map +1 -0
- package/dist/actions/get-tunnel-status.d.ts +4 -0
- package/dist/actions/get-tunnel-status.d.ts.map +1 -0
- package/dist/actions/get-tunnel-status.js +186 -0
- package/dist/actions/get-tunnel-status.js.map +1 -0
- package/dist/actions/start-tunnel.d.ts +4 -0
- package/dist/actions/start-tunnel.d.ts.map +1 -0
- package/dist/actions/start-tunnel.js +221 -0
- package/dist/actions/start-tunnel.js.map +1 -0
- package/dist/actions/stop-tunnel.d.ts +4 -0
- package/dist/actions/stop-tunnel.d.ts.map +1 -0
- package/dist/actions/stop-tunnel.js +174 -0
- package/dist/actions/stop-tunnel.js.map +1 -0
- package/dist/environment.d.ts +12 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +68 -0
- package/dist/environment.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/services/NgrokService.d.ts +30 -0
- package/dist/services/NgrokService.d.ts.map +1 -0
- package/dist/services/NgrokService.js +333 -0
- package/dist/services/NgrokService.js.map +1 -0
- package/package.json +63 -0
- package/src/__tests__/NgrokTestSuite.ts +110 -0
- package/src/__tests__/debug-mock.test.ts +15 -0
- package/src/__tests__/e2e/real-ngrok.test.ts +543 -0
- package/src/__tests__/integration/webhook-scenarios.test.ts +463 -0
- package/src/__tests__/mocks/NgrokServiceMock.ts +76 -0
- package/src/__tests__/ngrok-integration.test.ts +521 -0
- package/src/__tests__/test-config.ts +83 -0
- package/src/__tests__/test-helpers.ts +43 -0
- package/src/__tests__/test-setup.ts +174 -0
- package/src/__tests__/test-utils.ts +155 -0
- package/src/__tests__/unit/actions.test.ts +402 -0
- package/src/__tests__/unit/environment.test.ts +352 -0
- package/src/actions/get-tunnel-status.ts +218 -0
- package/src/actions/start-tunnel.ts +255 -0
- package/src/actions/stop-tunnel.ts +203 -0
- package/src/environment.ts +75 -0
- package/src/index.ts +33 -0
- package/src/services/NgrokService.ts +401 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
2
|
+
import type { HandlerCallback, IAgentRuntime, Memory, State } from '@elizaos/core';
|
|
3
|
+
|
|
4
|
+
type SimpleMock<TArgs extends readonly unknown[] = unknown[], TReturn = unknown> = ((
|
|
5
|
+
...args: TArgs
|
|
6
|
+
) => TReturn) & {
|
|
7
|
+
calls: TArgs[];
|
|
8
|
+
_returnValue: TReturn | undefined;
|
|
9
|
+
_implementation: ((...args: unknown[]) => TReturn) | null;
|
|
10
|
+
mockReturnValue: (value: TReturn) => SimpleMock<TArgs, TReturn>;
|
|
11
|
+
mockResolvedValue: (value: Awaited<TReturn>) => SimpleMock<TArgs, TReturn>;
|
|
12
|
+
mockRejectedValue: (reason: unknown) => SimpleMock<TArgs, TReturn>;
|
|
13
|
+
mockImplementation: (impl: (...args: unknown[]) => TReturn) => SimpleMock<TArgs, TReturn>;
|
|
14
|
+
mock: { calls: TArgs[]; results: unknown[] };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Use local mock implementation until core test-utils build issue is resolved
|
|
18
|
+
const mock = <const TArgs extends readonly unknown[] = unknown[], TReturn = unknown>(): SimpleMock<
|
|
19
|
+
TArgs,
|
|
20
|
+
TReturn
|
|
21
|
+
> => {
|
|
22
|
+
const calls: TArgs[] = [];
|
|
23
|
+
const fn = ((...args: TArgs) => {
|
|
24
|
+
calls.push(args);
|
|
25
|
+
if (typeof fn._implementation === 'function') {
|
|
26
|
+
return fn._implementation(...args);
|
|
27
|
+
}
|
|
28
|
+
return fn._returnValue as TReturn;
|
|
29
|
+
}) as SimpleMock<TArgs, TReturn>;
|
|
30
|
+
fn.calls = calls;
|
|
31
|
+
fn._returnValue = undefined;
|
|
32
|
+
fn._implementation = null;
|
|
33
|
+
fn.mockReturnValue = (value: TReturn) => {
|
|
34
|
+
fn._returnValue = value;
|
|
35
|
+
fn._implementation = null;
|
|
36
|
+
return fn;
|
|
37
|
+
};
|
|
38
|
+
fn.mockResolvedValue = (value: Awaited<TReturn>) => {
|
|
39
|
+
fn._returnValue = Promise.resolve(value) as TReturn;
|
|
40
|
+
fn._implementation = null;
|
|
41
|
+
return fn;
|
|
42
|
+
};
|
|
43
|
+
fn.mockRejectedValue = (reason: unknown) => {
|
|
44
|
+
fn._returnValue = Promise.reject(reason) as TReturn;
|
|
45
|
+
fn._implementation = null;
|
|
46
|
+
return fn;
|
|
47
|
+
};
|
|
48
|
+
fn.mockImplementation = (impl: (...args: unknown[]) => TReturn) => {
|
|
49
|
+
fn._implementation = impl;
|
|
50
|
+
fn._returnValue = undefined;
|
|
51
|
+
return fn;
|
|
52
|
+
};
|
|
53
|
+
fn.mock = { calls, results: [] };
|
|
54
|
+
return fn;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
import { getTunnelStatusAction } from '../../actions/get-tunnel-status';
|
|
58
|
+
import { startTunnelAction } from '../../actions/start-tunnel';
|
|
59
|
+
import { stopTunnelAction } from '../../actions/stop-tunnel';
|
|
60
|
+
import { MockNgrokService } from '../mocks/NgrokServiceMock';
|
|
61
|
+
import { createMockMemory, createMockRuntime, createMockState } from '../test-utils';
|
|
62
|
+
|
|
63
|
+
describe('Ngrok Actions - Validation and Error Handling', () => {
|
|
64
|
+
let mockRuntime: IAgentRuntime;
|
|
65
|
+
let mockTunnelService: MockNgrokService;
|
|
66
|
+
let mockCallback: HandlerCallback;
|
|
67
|
+
let mockMemory: Memory;
|
|
68
|
+
let mockState: State;
|
|
69
|
+
let mockUseModel: ReturnType<typeof mock>;
|
|
70
|
+
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
mockTunnelService = new MockNgrokService({} as IAgentRuntime);
|
|
73
|
+
|
|
74
|
+
// Reset all mocks to default behavior
|
|
75
|
+
mockTunnelService.startTunnel.mockResolvedValue('https://test.ngrok.io');
|
|
76
|
+
mockTunnelService.stopTunnel.mockResolvedValue(undefined);
|
|
77
|
+
mockTunnelService.isActive.mockReturnValue(false);
|
|
78
|
+
mockTunnelService.getStatus.mockReturnValue({
|
|
79
|
+
active: false,
|
|
80
|
+
url: null,
|
|
81
|
+
port: null,
|
|
82
|
+
startedAt: null,
|
|
83
|
+
provider: 'ngrok',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
mockUseModel = mock().mockResolvedValue('{"port": 8080}');
|
|
87
|
+
mockRuntime = createMockRuntime({
|
|
88
|
+
getService: mock().mockImplementation((name: string) => {
|
|
89
|
+
if (name === 'tunnel' || name === 'ngrok-tunnel') {
|
|
90
|
+
return mockTunnelService;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}),
|
|
94
|
+
useModel: mockUseModel,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
mockCallback = mock();
|
|
98
|
+
mockMemory = createMockMemory();
|
|
99
|
+
mockState = createMockState();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
afterEach(() => {
|
|
103
|
+
// Clear any mock state between tests
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('startTunnelAction - Validation', () => {
|
|
107
|
+
it('should validate that tunnel service exists', async () => {
|
|
108
|
+
const runtimeWithoutService = createMockRuntime({
|
|
109
|
+
getService: mock().mockReturnValue(null),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const isValid = await startTunnelAction.validate(runtimeWithoutService, mockMemory);
|
|
113
|
+
|
|
114
|
+
expect(isValid).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should validate when tunnel service is available', async () => {
|
|
118
|
+
const isValid = await startTunnelAction.validate(mockRuntime, mockMemory);
|
|
119
|
+
|
|
120
|
+
expect(isValid).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('startTunnelAction - Error Handling', () => {
|
|
125
|
+
it('should handle service not available', async () => {
|
|
126
|
+
const runtimeWithoutService = createMockRuntime({
|
|
127
|
+
getService: mock().mockReturnValue(null),
|
|
128
|
+
});
|
|
129
|
+
const memory = createMockMemory({
|
|
130
|
+
content: { text: 'start tunnel on port 8080' },
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const result = await startTunnelAction.handler(
|
|
134
|
+
runtimeWithoutService,
|
|
135
|
+
memory,
|
|
136
|
+
mockState,
|
|
137
|
+
{},
|
|
138
|
+
mockCallback
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
expect(result.values?.success).toBe(false);
|
|
142
|
+
expect(result.values?.error).toBe('service_unavailable');
|
|
143
|
+
expect(mockCallback.calls.length).toBeGreaterThan(0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should handle invalid port numbers gracefully', async () => {
|
|
147
|
+
const runtimeWithInvalidPort = createMockRuntime({
|
|
148
|
+
getService: mock().mockImplementation((name: string) => {
|
|
149
|
+
if (name === 'tunnel' || name === 'ngrok-tunnel') {
|
|
150
|
+
return mockTunnelService;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}),
|
|
154
|
+
useModel: mock().mockResolvedValue('{"port": -1}'),
|
|
155
|
+
});
|
|
156
|
+
const memory = createMockMemory({
|
|
157
|
+
content: { text: 'start tunnel on port -1' },
|
|
158
|
+
});
|
|
159
|
+
mockTunnelService.startTunnel.mockResolvedValue('https://test.ngrok.io');
|
|
160
|
+
|
|
161
|
+
const result = await startTunnelAction.handler(
|
|
162
|
+
runtimeWithInvalidPort,
|
|
163
|
+
memory,
|
|
164
|
+
mockState,
|
|
165
|
+
{},
|
|
166
|
+
mockCallback
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(result.values?.success).toBe(true);
|
|
170
|
+
expect(result.values?.tunnelUrl).toContain('ngrok.io');
|
|
171
|
+
expect(mockTunnelService.startTunnel.calls.length).toBeGreaterThan(0);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should handle port extraction failure', async () => {
|
|
175
|
+
mockMemory.content = { text: 'start tunnel' };
|
|
176
|
+
mockUseModel.mockResolvedValue('invalid json');
|
|
177
|
+
mockTunnelService.startTunnel.mockResolvedValue('https://test.ngrok.io');
|
|
178
|
+
|
|
179
|
+
const result = await startTunnelAction.handler(
|
|
180
|
+
mockRuntime,
|
|
181
|
+
mockMemory,
|
|
182
|
+
mockState,
|
|
183
|
+
{},
|
|
184
|
+
mockCallback
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
expect(result.values?.success).toBe(true);
|
|
188
|
+
expect(result.values?.tunnelUrl).toContain('ngrok.io');
|
|
189
|
+
expect(mockTunnelService.startTunnel.calls.length).toBeGreaterThan(0);
|
|
190
|
+
expect(mockTunnelService.startTunnel.calls[0][0]).toBe(3000); // Should use default
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should handle tunnel start failure', async () => {
|
|
194
|
+
// Create a fresh mock service for this test that will fail
|
|
195
|
+
const failingTunnelService = new MockNgrokService({} as IAgentRuntime);
|
|
196
|
+
failingTunnelService.startTunnel.mockRejectedValue(new Error('Ngrok auth failed'));
|
|
197
|
+
failingTunnelService.isActive.mockReturnValue(false);
|
|
198
|
+
|
|
199
|
+
const failingRuntime = createMockRuntime({
|
|
200
|
+
getService: mock().mockImplementation((name: string) => {
|
|
201
|
+
if (name === 'tunnel' || name === 'ngrok-tunnel') {
|
|
202
|
+
return failingTunnelService;
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}),
|
|
206
|
+
useModel: mock().mockResolvedValue('{"port": 8080}'),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
mockMemory.content = { text: 'start tunnel on port 8080' };
|
|
210
|
+
|
|
211
|
+
const result = await startTunnelAction.handler(
|
|
212
|
+
failingRuntime,
|
|
213
|
+
mockMemory,
|
|
214
|
+
mockState,
|
|
215
|
+
{},
|
|
216
|
+
mockCallback
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
expect(result.values?.success).toBe(false);
|
|
220
|
+
expect(result.values?.error).toBe('Ngrok auth failed');
|
|
221
|
+
expect(mockCallback.calls.length).toBeGreaterThan(0);
|
|
222
|
+
const call = mockCallback.calls[0][0];
|
|
223
|
+
expect(call.text).toContain('Failed to start ngrok tunnel');
|
|
224
|
+
expect(call.metadata.error).toBe('Ngrok auth failed');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should handle port already in use', async () => {
|
|
228
|
+
// Create a fresh mock service for this test that reports as active
|
|
229
|
+
const activeTunnelService = new MockNgrokService({} as IAgentRuntime);
|
|
230
|
+
activeTunnelService.isActive.mockReturnValue(true);
|
|
231
|
+
activeTunnelService.getStatus.mockReturnValue({
|
|
232
|
+
active: true,
|
|
233
|
+
url: 'https://existing.ngrok.io',
|
|
234
|
+
port: 8080,
|
|
235
|
+
startedAt: new Date(),
|
|
236
|
+
provider: 'ngrok',
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const activeRuntime = createMockRuntime({
|
|
240
|
+
getService: mock().mockImplementation((name: string) => {
|
|
241
|
+
if (name === 'tunnel' || name === 'ngrok-tunnel') {
|
|
242
|
+
return activeTunnelService;
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}),
|
|
246
|
+
useModel: mock().mockResolvedValue('{"port": 8080}'),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
mockMemory.content = { text: 'start tunnel on port 8080' };
|
|
250
|
+
|
|
251
|
+
const result = await startTunnelAction.handler(
|
|
252
|
+
activeRuntime,
|
|
253
|
+
mockMemory,
|
|
254
|
+
mockState,
|
|
255
|
+
{},
|
|
256
|
+
mockCallback
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
expect(result.values?.success).toBe(false);
|
|
260
|
+
expect(result.values?.error).toBe('tunnel_already_active');
|
|
261
|
+
expect(mockCallback.calls.length).toBeGreaterThan(0);
|
|
262
|
+
expect(mockCallback.calls[0][0].text).toContain('Tunnel is already active');
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe('stopTunnelAction - Validation', () => {
|
|
267
|
+
it('should validate that tunnel service exists', async () => {
|
|
268
|
+
const runtimeWithoutService = createMockRuntime({
|
|
269
|
+
getService: mock().mockReturnValue(null),
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const isValid = await stopTunnelAction.validate(runtimeWithoutService, mockMemory);
|
|
273
|
+
|
|
274
|
+
expect(isValid).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('stopTunnelAction - Error Handling', () => {
|
|
279
|
+
it('should handle service not available', async () => {
|
|
280
|
+
const runtimeWithoutService = createMockRuntime({
|
|
281
|
+
getService: mock().mockReturnValue(null),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const result = await stopTunnelAction.handler(
|
|
285
|
+
runtimeWithoutService,
|
|
286
|
+
mockMemory,
|
|
287
|
+
mockState,
|
|
288
|
+
{},
|
|
289
|
+
mockCallback
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
expect(result.values?.success).toBe(false);
|
|
293
|
+
expect(result.values?.error).toBe('service_unavailable');
|
|
294
|
+
expect(mockCallback.calls.length).toBeGreaterThan(0);
|
|
295
|
+
expect(mockCallback.calls[0][0].text).toContain('Tunnel service is not available');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should handle stopping when no tunnel is active', async () => {
|
|
299
|
+
mockTunnelService.isActive.mockReturnValue(false);
|
|
300
|
+
|
|
301
|
+
const result = await stopTunnelAction.handler(
|
|
302
|
+
mockRuntime,
|
|
303
|
+
mockMemory,
|
|
304
|
+
mockState,
|
|
305
|
+
{},
|
|
306
|
+
mockCallback
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
expect(result.values?.success).toBe(true);
|
|
310
|
+
expect(result.values?.wasActive).toBe(false);
|
|
311
|
+
expect(mockCallback.calls.length).toBeGreaterThan(0);
|
|
312
|
+
expect(mockCallback.calls[0][0].text).toContain('No tunnel is currently running');
|
|
313
|
+
expect(mockTunnelService.stopTunnel.calls.length).toBe(0);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should handle stop failure gracefully', async () => {
|
|
317
|
+
// Create a fresh mock service for this test that will fail to stop
|
|
318
|
+
const failingStopService = new MockNgrokService({} as IAgentRuntime);
|
|
319
|
+
failingStopService.isActive.mockReturnValue(true);
|
|
320
|
+
failingStopService.getStatus.mockReturnValue({
|
|
321
|
+
active: true,
|
|
322
|
+
url: 'https://test.ngrok.io',
|
|
323
|
+
port: 8080,
|
|
324
|
+
startedAt: new Date(),
|
|
325
|
+
provider: 'ngrok',
|
|
326
|
+
});
|
|
327
|
+
failingStopService.stopTunnel.mockRejectedValue(new Error('Stop failed'));
|
|
328
|
+
|
|
329
|
+
const failingStopRuntime = createMockRuntime({
|
|
330
|
+
getService: mock().mockImplementation((name: string) => {
|
|
331
|
+
if (name === 'tunnel' || name === 'ngrok-tunnel') {
|
|
332
|
+
return failingStopService;
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
}),
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const result = await stopTunnelAction.handler(
|
|
339
|
+
failingStopRuntime,
|
|
340
|
+
mockMemory,
|
|
341
|
+
mockState,
|
|
342
|
+
{},
|
|
343
|
+
mockCallback
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
expect(result.values?.success).toBe(false);
|
|
347
|
+
expect(result.values?.error).toBe('Stop failed');
|
|
348
|
+
expect(mockCallback.calls.length).toBeGreaterThan(0);
|
|
349
|
+
const call = mockCallback.calls[0][0];
|
|
350
|
+
expect(call.text).toContain('Failed to stop ngrok tunnel');
|
|
351
|
+
expect(call.metadata.error).toBe('Stop failed');
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe('getTunnelStatusAction - Edge Cases', () => {
|
|
356
|
+
it('should handle service not available', async () => {
|
|
357
|
+
const runtimeWithoutService = createMockRuntime({
|
|
358
|
+
getService: mock().mockReturnValue(null),
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const result = await getTunnelStatusAction.handler(
|
|
362
|
+
runtimeWithoutService,
|
|
363
|
+
mockMemory,
|
|
364
|
+
mockState,
|
|
365
|
+
{},
|
|
366
|
+
mockCallback
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
expect(result.values?.success).toBe(false);
|
|
370
|
+
expect(result.values?.error).toBe('Tunnel service not found');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should format uptime correctly', async () => {
|
|
374
|
+
const startTime = new Date();
|
|
375
|
+
startTime.setHours(startTime.getHours() - 2); // 2 hours ago
|
|
376
|
+
|
|
377
|
+
// Create a fresh mock service for this test with active tunnel
|
|
378
|
+
const activeTunnelService = new MockNgrokService({} as IAgentRuntime);
|
|
379
|
+
activeTunnelService.getStatus.mockReturnValue({
|
|
380
|
+
active: true,
|
|
381
|
+
url: 'https://test.ngrok.io',
|
|
382
|
+
port: 8080,
|
|
383
|
+
startedAt: startTime,
|
|
384
|
+
provider: 'ngrok',
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const activeRuntime = createMockRuntime({
|
|
388
|
+
getService: mock().mockImplementation((name: string) => {
|
|
389
|
+
if (name === 'tunnel' || name === 'ngrok-tunnel') {
|
|
390
|
+
return activeTunnelService;
|
|
391
|
+
}
|
|
392
|
+
return null;
|
|
393
|
+
}),
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
await getTunnelStatusAction.handler(activeRuntime, mockMemory, mockState, {}, mockCallback);
|
|
397
|
+
|
|
398
|
+
expect(mockCallback.calls.length).toBeGreaterThan(0);
|
|
399
|
+
expect(mockCallback.calls[0][0].text).toContain('2 hours');
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
});
|