@knocklabs/client 0.14.10-canary.2 โ†’ 0.14.10

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.
Files changed (129) hide show
  1. package/CHANGELOG.md +326 -0
  2. package/package.json +3 -3
  3. package/test/README.md +590 -0
  4. package/dist/cjs/api.js +0 -2
  5. package/dist/cjs/api.js.map +0 -1
  6. package/dist/cjs/clients/feed/feed.js +0 -2
  7. package/dist/cjs/clients/feed/feed.js.map +0 -1
  8. package/dist/cjs/clients/feed/index.js +0 -2
  9. package/dist/cjs/clients/feed/index.js.map +0 -1
  10. package/dist/cjs/clients/feed/socket-manager.js +0 -2
  11. package/dist/cjs/clients/feed/socket-manager.js.map +0 -1
  12. package/dist/cjs/clients/feed/store.js +0 -2
  13. package/dist/cjs/clients/feed/store.js.map +0 -1
  14. package/dist/cjs/clients/feed/utils.js +0 -2
  15. package/dist/cjs/clients/feed/utils.js.map +0 -1
  16. package/dist/cjs/clients/guide/client.js +0 -2
  17. package/dist/cjs/clients/guide/client.js.map +0 -1
  18. package/dist/cjs/clients/messages/index.js +0 -2
  19. package/dist/cjs/clients/messages/index.js.map +0 -1
  20. package/dist/cjs/clients/ms-teams/index.js +0 -2
  21. package/dist/cjs/clients/ms-teams/index.js.map +0 -1
  22. package/dist/cjs/clients/objects/constants.js +0 -2
  23. package/dist/cjs/clients/objects/constants.js.map +0 -1
  24. package/dist/cjs/clients/objects/index.js +0 -2
  25. package/dist/cjs/clients/objects/index.js.map +0 -1
  26. package/dist/cjs/clients/preferences/index.js +0 -2
  27. package/dist/cjs/clients/preferences/index.js.map +0 -1
  28. package/dist/cjs/clients/slack/index.js +0 -2
  29. package/dist/cjs/clients/slack/index.js.map +0 -1
  30. package/dist/cjs/clients/users/index.js +0 -2
  31. package/dist/cjs/clients/users/index.js.map +0 -1
  32. package/dist/cjs/helpers.js +0 -2
  33. package/dist/cjs/helpers.js.map +0 -1
  34. package/dist/cjs/index.js +0 -2
  35. package/dist/cjs/index.js.map +0 -1
  36. package/dist/cjs/knock.js +0 -2
  37. package/dist/cjs/knock.js.map +0 -1
  38. package/dist/cjs/networkStatus.js +0 -2
  39. package/dist/cjs/networkStatus.js.map +0 -1
  40. package/dist/esm/api.mjs +0 -58
  41. package/dist/esm/api.mjs.map +0 -1
  42. package/dist/esm/clients/feed/feed.mjs +0 -422
  43. package/dist/esm/clients/feed/feed.mjs.map +0 -1
  44. package/dist/esm/clients/feed/index.mjs +0 -47
  45. package/dist/esm/clients/feed/index.mjs.map +0 -1
  46. package/dist/esm/clients/feed/socket-manager.mjs +0 -81
  47. package/dist/esm/clients/feed/socket-manager.mjs.map +0 -1
  48. package/dist/esm/clients/feed/store.mjs +0 -104
  49. package/dist/esm/clients/feed/store.mjs.map +0 -1
  50. package/dist/esm/clients/feed/utils.mjs +0 -35
  51. package/dist/esm/clients/feed/utils.mjs.map +0 -1
  52. package/dist/esm/clients/guide/client.mjs +0 -284
  53. package/dist/esm/clients/guide/client.mjs.map +0 -1
  54. package/dist/esm/clients/messages/index.mjs +0 -64
  55. package/dist/esm/clients/messages/index.mjs.map +0 -1
  56. package/dist/esm/clients/ms-teams/index.mjs +0 -91
  57. package/dist/esm/clients/ms-teams/index.mjs.map +0 -1
  58. package/dist/esm/clients/objects/constants.mjs +0 -5
  59. package/dist/esm/clients/objects/constants.mjs.map +0 -1
  60. package/dist/esm/clients/objects/index.mjs +0 -42
  61. package/dist/esm/clients/objects/index.mjs.map +0 -1
  62. package/dist/esm/clients/preferences/index.mjs +0 -128
  63. package/dist/esm/clients/preferences/index.mjs.map +0 -1
  64. package/dist/esm/clients/slack/index.mjs +0 -72
  65. package/dist/esm/clients/slack/index.mjs.map +0 -1
  66. package/dist/esm/clients/users/index.mjs +0 -99
  67. package/dist/esm/clients/users/index.mjs.map +0 -1
  68. package/dist/esm/helpers.mjs +0 -8
  69. package/dist/esm/helpers.mjs.map +0 -1
  70. package/dist/esm/index.mjs +0 -16
  71. package/dist/esm/index.mjs.map +0 -1
  72. package/dist/esm/knock.mjs +0 -95
  73. package/dist/esm/knock.mjs.map +0 -1
  74. package/dist/esm/networkStatus.mjs +0 -15
  75. package/dist/esm/networkStatus.mjs.map +0 -1
  76. package/dist/types/api.d.ts +0 -25
  77. package/dist/types/api.d.ts.map +0 -1
  78. package/dist/types/clients/feed/feed.d.ts +0 -75
  79. package/dist/types/clients/feed/feed.d.ts.map +0 -1
  80. package/dist/types/clients/feed/index.d.ts +0 -17
  81. package/dist/types/clients/feed/index.d.ts.map +0 -1
  82. package/dist/types/clients/feed/interfaces.d.ts +0 -99
  83. package/dist/types/clients/feed/interfaces.d.ts.map +0 -1
  84. package/dist/types/clients/feed/socket-manager.d.ts +0 -31
  85. package/dist/types/clients/feed/socket-manager.d.ts.map +0 -1
  86. package/dist/types/clients/feed/store.d.ts +0 -20
  87. package/dist/types/clients/feed/store.d.ts.map +0 -1
  88. package/dist/types/clients/feed/types.d.ts +0 -35
  89. package/dist/types/clients/feed/types.d.ts.map +0 -1
  90. package/dist/types/clients/feed/utils.d.ts +0 -20
  91. package/dist/types/clients/feed/utils.d.ts.map +0 -1
  92. package/dist/types/clients/guide/client.d.ts +0 -124
  93. package/dist/types/clients/guide/client.d.ts.map +0 -1
  94. package/dist/types/clients/guide/index.d.ts +0 -3
  95. package/dist/types/clients/guide/index.d.ts.map +0 -1
  96. package/dist/types/clients/messages/index.d.ts +0 -15
  97. package/dist/types/clients/messages/index.d.ts.map +0 -1
  98. package/dist/types/clients/messages/interfaces.d.ts +0 -46
  99. package/dist/types/clients/messages/interfaces.d.ts.map +0 -1
  100. package/dist/types/clients/ms-teams/index.d.ts +0 -14
  101. package/dist/types/clients/ms-teams/index.d.ts.map +0 -1
  102. package/dist/types/clients/ms-teams/interfaces.d.ts +0 -49
  103. package/dist/types/clients/ms-teams/interfaces.d.ts.map +0 -1
  104. package/dist/types/clients/objects/constants.d.ts +0 -2
  105. package/dist/types/clients/objects/constants.d.ts.map +0 -1
  106. package/dist/types/clients/objects/index.d.ts +0 -23
  107. package/dist/types/clients/objects/index.d.ts.map +0 -1
  108. package/dist/types/clients/preferences/index.d.ts +0 -46
  109. package/dist/types/clients/preferences/index.d.ts.map +0 -1
  110. package/dist/types/clients/preferences/interfaces.d.ts +0 -29
  111. package/dist/types/clients/preferences/interfaces.d.ts.map +0 -1
  112. package/dist/types/clients/slack/index.d.ts +0 -13
  113. package/dist/types/clients/slack/index.d.ts.map +0 -1
  114. package/dist/types/clients/slack/interfaces.d.ts +0 -29
  115. package/dist/types/clients/slack/interfaces.d.ts.map +0 -1
  116. package/dist/types/clients/users/index.d.ts +0 -22
  117. package/dist/types/clients/users/index.d.ts.map +0 -1
  118. package/dist/types/clients/users/interfaces.d.ts +0 -9
  119. package/dist/types/clients/users/interfaces.d.ts.map +0 -1
  120. package/dist/types/helpers.d.ts +0 -2
  121. package/dist/types/helpers.d.ts.map +0 -1
  122. package/dist/types/index.d.ts +0 -21
  123. package/dist/types/index.d.ts.map +0 -1
  124. package/dist/types/interfaces.d.ts +0 -66
  125. package/dist/types/interfaces.d.ts.map +0 -1
  126. package/dist/types/knock.d.ts +0 -39
  127. package/dist/types/knock.d.ts.map +0 -1
  128. package/dist/types/networkStatus.d.ts +0 -8
  129. package/dist/types/networkStatus.d.ts.map +0 -1
package/test/README.md ADDED
@@ -0,0 +1,590 @@
1
+ # Testing Guide for Knock Client
2
+
3
+ This directory contains all tests for the Knock JavaScript client. This guide will help you understand our testing patterns, utilities, and how to write effective tests.
4
+
5
+ ## ๐Ÿ“ Test Structure
6
+
7
+ ```
8
+ test/
9
+ โ”œโ”€โ”€ README.md # This guide
10
+ โ”œโ”€โ”€ setup.ts # Global test configuration
11
+ โ”œโ”€โ”€ test-utils/ # Shared testing utilities
12
+ โ”‚ โ”œโ”€โ”€ fixtures.ts # Test data generators
13
+ โ”‚ โ”œโ”€โ”€ mocks.ts # Mock factories
14
+ โ”‚ โ””โ”€โ”€ property-testing.ts # Property-based testing tools
15
+ โ”œโ”€โ”€ clients/ # Client-specific tests
16
+ โ”‚ โ”œโ”€โ”€ feed/ # Feed client tests
17
+ โ”‚ โ”œโ”€โ”€ messages/ # Messages client tests
18
+ โ”‚ โ”œโ”€โ”€ users/ # Users client tests
19
+ โ”‚ โ””โ”€โ”€ ... # Other client tests
20
+ โ”œโ”€โ”€ knock.test.ts # Main Knock class tests
21
+ โ”œโ”€โ”€ api.test.ts # API client tests
22
+ โ”œโ”€โ”€ helpers.test.ts # Utility functions tests
23
+ โ””โ”€โ”€ ... # Other core tests
24
+ ```
25
+
26
+ ## ๐Ÿš€ Quick Start: Writing Your First Test
27
+
28
+ Here's a simple test to get you started:
29
+
30
+ ```typescript
31
+ import { describe, expect, test } from "vitest";
32
+
33
+ import { createMockKnock } from "./test-utils/mocks";
34
+
35
+ describe("My Feature", () => {
36
+ test("should do something", () => {
37
+ const { knock } = createMockKnock();
38
+
39
+ // Your test logic here
40
+ expect(knock).toBeDefined();
41
+ });
42
+ });
43
+ ```
44
+
45
+ **Important:** Always add `` at the top of test files.
46
+
47
+ ## ๐Ÿ›  Test Utilities
48
+
49
+ ### 1. Fixtures (`test-utils/fixtures.ts`)
50
+
51
+ Fixtures create realistic test data. Use them instead of manually creating objects.
52
+
53
+ **Feed Items:**
54
+
55
+ ```typescript
56
+ import {
57
+ createArchivedFeedItem,
58
+ createMockFeedItem,
59
+ createReadFeedItem,
60
+ createUnreadFeedItem,
61
+ } from "./test-utils/fixtures";
62
+
63
+ // Create a basic feed item
64
+ const item = createMockFeedItem();
65
+
66
+ // Create specific states
67
+ const unreadItem = createUnreadFeedItem();
68
+ const readItem = createReadFeedItem({
69
+ read_at: "2024-01-01T00:00:00Z",
70
+ });
71
+
72
+ // Create multiple items
73
+ const items = createMockFeedItems(5);
74
+ ```
75
+
76
+ **Messages:**
77
+
78
+ ```typescript
79
+ import {
80
+ createMockMessage,
81
+ createReadMessage,
82
+ createUnreadMessage,
83
+ } from "./test-utils/fixtures";
84
+
85
+ const message = createMockMessage({
86
+ id: "custom-id",
87
+ });
88
+ ```
89
+
90
+ **Users:**
91
+
92
+ ```typescript
93
+ import { createMockUser, createMockUsers } from "./test-utils/fixtures";
94
+
95
+ const user = createMockUser({ name: "John Doe" });
96
+ const users = createMockUsers(10);
97
+ ```
98
+
99
+ **Complex Scenarios:**
100
+
101
+ ```typescript
102
+ import {
103
+ createBulkOperationScenario,
104
+ createErrorRecoveryScenario,
105
+ createUserJourneyScenario,
106
+ } from "./test-utils/fixtures";
107
+
108
+ // Pre-built realistic test scenarios
109
+ const scenario = createUserJourneyScenario();
110
+ ```
111
+
112
+ ### 2. Mocks (`test-utils/mocks.ts`)
113
+
114
+ Mocks handle external dependencies and API calls.
115
+
116
+ **Basic Setup:**
117
+
118
+ ```typescript
119
+ import { authenticateKnock, createMockKnock } from "./test-utils/mocks";
120
+
121
+ test("my test", () => {
122
+ const { knock, mockApiClient } = createMockKnock();
123
+
124
+ // Authenticate if needed
125
+ authenticateKnock(knock);
126
+
127
+ // Your test logic
128
+ });
129
+ ```
130
+
131
+ **API Mocking:**
132
+
133
+ ```typescript
134
+ import {
135
+ mockNetworkError,
136
+ mockNetworkFailure,
137
+ mockNetworkSuccess,
138
+ } from "./test-utils/mocks";
139
+
140
+ test("handles successful API call", async () => {
141
+ const { knock, mockApiClient } = createMockKnock();
142
+
143
+ // Mock successful response
144
+ mockNetworkSuccess(mockApiClient, { data: "success" });
145
+
146
+ // Test your code
147
+ });
148
+
149
+ test("handles API error", async () => {
150
+ const { knock, mockApiClient } = createMockKnock();
151
+
152
+ // Mock error response
153
+ mockNetworkError(mockApiClient, 400, "Bad Request");
154
+
155
+ // Test error handling
156
+ });
157
+
158
+ test("handles network failure", async () => {
159
+ const { knock, mockApiClient } = createMockKnock();
160
+
161
+ // Mock network failure
162
+ mockNetworkFailure(mockApiClient, new Error("Network down"));
163
+
164
+ // Test failure handling
165
+ });
166
+ ```
167
+
168
+ **Feed Mocking:**
169
+
170
+ ```typescript
171
+ import { createMockFeed } from "./test-utils/mocks";
172
+
173
+ test("feed operations", () => {
174
+ const { feed, mockApiClient, mockSocketManager } = createMockFeed(
175
+ "test-feed-id",
176
+ { page_size: 25 },
177
+ );
178
+
179
+ // Test feed operations
180
+ });
181
+ ```
182
+
183
+ ### 3. Property Testing (`test-utils/property-testing.ts`)
184
+
185
+ Property testing helps find edge cases by testing with generated data.
186
+
187
+ ```typescript
188
+ import {
189
+ feedItemArbitrary,
190
+ generators,
191
+ property,
192
+ } from "./test-utils/property-testing";
193
+
194
+ test("property: all feed items should have valid IDs", async () => {
195
+ const result = await property.forAll(
196
+ feedItemArbitrary(),
197
+ (item) => item.id.length > 0,
198
+ );
199
+
200
+ expect(result.success).toBe(true);
201
+ });
202
+
203
+ test("property: numbers are always positive", async () => {
204
+ const result = await property.forAll(
205
+ generators.number(1, 1000),
206
+ (num) => num > 0,
207
+ );
208
+
209
+ expect(result.success).toBe(true);
210
+ });
211
+ ```
212
+
213
+ ## ๐Ÿ“ Testing Patterns
214
+
215
+ ### 1. Test Organization
216
+
217
+ **Use descriptive describe blocks:**
218
+
219
+ ```typescript
220
+ describe("Feed Client", () => {
221
+ describe("Initialization", () => {
222
+ test("creates feed with valid options", () => {
223
+ // Test initialization
224
+ });
225
+ });
226
+
227
+ describe("Data Operations", () => {
228
+ test("fetches feed items successfully", () => {
229
+ // Test data fetching
230
+ });
231
+ });
232
+
233
+ describe("Error Handling", () => {
234
+ test("handles network errors gracefully", () => {
235
+ // Test error scenarios
236
+ });
237
+ });
238
+ });
239
+ ```
240
+
241
+ ### 2. Setup and Cleanup
242
+
243
+ **Use consistent setup:**
244
+
245
+ ```typescript
246
+ import { afterEach, beforeEach, describe, test, vi } from "vitest";
247
+
248
+ describe("My Feature", () => {
249
+ const getTestSetup = () => {
250
+ const { knock, mockApiClient } = createMockKnock();
251
+ authenticateKnock(knock);
252
+
253
+ return {
254
+ knock,
255
+ mockApiClient,
256
+ cleanup: () => vi.clearAllMocks(),
257
+ };
258
+ };
259
+
260
+ afterEach(() => {
261
+ vi.clearAllMocks();
262
+ });
263
+
264
+ test("my test", () => {
265
+ const { knock, mockApiClient, cleanup } = getTestSetup();
266
+
267
+ try {
268
+ // Your test logic
269
+ } finally {
270
+ cleanup();
271
+ }
272
+ });
273
+ });
274
+ ```
275
+
276
+ ### 3. Async Testing
277
+
278
+ **Handle promises correctly:**
279
+
280
+ ```typescript
281
+ test("async operation succeeds", async () => {
282
+ const { knock, mockApiClient } = createMockKnock();
283
+
284
+ mockNetworkSuccess(mockApiClient, { success: true });
285
+
286
+ const result = await knock.someAsyncOperation();
287
+
288
+ expect(result).toEqual({ success: true });
289
+ });
290
+
291
+ test("async operation fails", async () => {
292
+ const { knock, mockApiClient } = createMockKnock();
293
+
294
+ mockNetworkFailure(mockApiClient, new Error("Failed"));
295
+
296
+ await expect(knock.someAsyncOperation()).rejects.toThrow("Failed");
297
+ });
298
+ ```
299
+
300
+ ### 4. State Testing
301
+
302
+ **Test different states:**
303
+
304
+ ```typescript
305
+ test("handles unread items", () => {
306
+ const items = [
307
+ createUnreadFeedItem(),
308
+ createReadFeedItem(),
309
+ createUnreadFeedItem(),
310
+ ];
311
+
312
+ const unreadCount = items.filter((item) => !item.read_at).length;
313
+ expect(unreadCount).toBe(2);
314
+ });
315
+ ```
316
+
317
+ ## ๐ŸŽฏ Testing Specific Clients
318
+
319
+ ### Feed Client Tests
320
+
321
+ ```typescript
322
+ import { createMockFeedItems } from "./test-utils/fixtures";
323
+ import { createMockFeed } from "./test-utils/mocks";
324
+
325
+ test("feed fetches items", async () => {
326
+ const { feed, mockApiClient } = createMockFeed();
327
+ const items = createMockFeedItems(5);
328
+
329
+ mockNetworkSuccess(mockApiClient, {
330
+ entries: items,
331
+ page_info: { page_size: 50 },
332
+ });
333
+
334
+ await feed.fetch();
335
+
336
+ expect(feed.store.items).toHaveLength(5);
337
+ });
338
+ ```
339
+
340
+ ### Messages Client Tests
341
+
342
+ ```typescript
343
+ import { createMockMessage } from "./test-utils/fixtures";
344
+
345
+ test("messages client gets message", async () => {
346
+ const { knock, mockApiClient } = createMockKnock();
347
+ const message = createMockMessage();
348
+
349
+ mockNetworkSuccess(mockApiClient, message);
350
+
351
+ const result = await knock.messages.get(message.id);
352
+
353
+ expect(result).toEqual(message);
354
+ });
355
+ ```
356
+
357
+ ### User Client Tests
358
+
359
+ ```typescript
360
+ test("user client identifies user", async () => {
361
+ const { knock, mockApiClient } = createMockKnock();
362
+
363
+ mockNetworkSuccess(mockApiClient, { success: true });
364
+
365
+ await knock.user.identify("user_123", { name: "John" });
366
+
367
+ expect(mockApiClient.makeRequest).toHaveBeenCalledWith({
368
+ method: "PUT",
369
+ url: "/v1/users/user_123",
370
+ data: { name: "John" },
371
+ });
372
+ });
373
+ ```
374
+
375
+ ## ๐Ÿงช Advanced Testing
376
+
377
+ ### Error Scenarios
378
+
379
+ ```typescript
380
+ test("handles rate limiting", async () => {
381
+ const { knock, mockApiClient } = createMockKnock();
382
+
383
+ mockNetworkError(mockApiClient, 429, "Rate limited");
384
+
385
+ await expect(knock.someOperation()).rejects.toThrow();
386
+ });
387
+
388
+ test("retries on network failure", async () => {
389
+ const { knock, mockApiClient } = createMockKnock();
390
+
391
+ // First call fails, second succeeds
392
+ mockApiClient.makeRequest
393
+ .mockRejectedValueOnce(new Error("Network error"))
394
+ .mockResolvedValueOnce({ statusCode: "ok", body: { success: true } });
395
+
396
+ const result = await knock.someRetryableOperation();
397
+
398
+ expect(result).toEqual({ success: true });
399
+ expect(mockApiClient.makeRequest).toHaveBeenCalledTimes(2);
400
+ });
401
+ ```
402
+
403
+ ### Performance Testing
404
+
405
+ ```typescript
406
+ import { createLargeFeedDataset } from "./test-utils/fixtures";
407
+
408
+ test("handles large datasets efficiently", () => {
409
+ const { items, metadata } = createLargeFeedDataset(10000);
410
+
411
+ const startTime = performance.now();
412
+
413
+ // Test operation
414
+ const result = processLargeDataset(items);
415
+
416
+ const endTime = performance.now();
417
+
418
+ expect(result).toBeDefined();
419
+ expect(endTime - startTime).toBeLessThan(1000); // Should complete in < 1s
420
+ });
421
+ ```
422
+
423
+ ## ๐Ÿ”ง Configuration
424
+
425
+ ### Global Setup (`setup.ts`)
426
+
427
+ The setup file handles:
428
+
429
+ - Environment polyfills
430
+ - Console output suppression during tests
431
+ - Global error handling
432
+ - Browser API mocks (localStorage, sessionStorage)
433
+
434
+ You usually don't need to modify this file.
435
+
436
+ ### Environment
437
+
438
+ All tests should run in Node environment:
439
+
440
+ ```typescript
441
+
442
+ ```
443
+
444
+ ## ๐Ÿšจ Common Issues & Solutions
445
+
446
+ ### 1. Unhandled Promise Rejections
447
+
448
+ **Problem:** Tests fail with unhandled promise rejections.
449
+
450
+ **Solution:** Always handle promises properly:
451
+
452
+ ```typescript
453
+ // โŒ Bad
454
+ test("test", () => {
455
+ someAsyncFunction(); // Promise not handled
456
+ });
457
+
458
+ // โœ… Good
459
+ test("test", async () => {
460
+ await someAsyncFunction();
461
+ });
462
+
463
+ // โœ… Also good
464
+ test("test", () => {
465
+ return someAsyncFunction();
466
+ });
467
+ ```
468
+
469
+ ### 2. Mock Cleanup
470
+
471
+ **Problem:** Mocks from one test affect another.
472
+
473
+ **Solution:** Always clean up:
474
+
475
+ ```typescript
476
+ afterEach(() => {
477
+ vi.clearAllMocks();
478
+ vi.restoreAllMocks();
479
+ });
480
+ ```
481
+
482
+ ### 3. Authentication Required
483
+
484
+ **Problem:** Tests fail because client isn't authenticated.
485
+
486
+ **Solution:** Use `authenticateKnock`:
487
+
488
+ ```typescript
489
+ test("authenticated operation", () => {
490
+ const { knock } = createMockKnock();
491
+ authenticateKnock(knock); // Add this line
492
+
493
+ // Now test authenticated operations
494
+ });
495
+ ```
496
+
497
+ ### 4. Network Mocking
498
+
499
+ **Problem:** Real network calls in tests.
500
+
501
+ **Solution:** Always mock network calls:
502
+
503
+ ```typescript
504
+ test("API operation", async () => {
505
+ const { knock, mockApiClient } = createMockKnock();
506
+
507
+ // Mock the expected response
508
+ mockNetworkSuccess(mockApiClient, expectedData);
509
+
510
+ const result = await knock.apiOperation();
511
+
512
+ expect(result).toEqual(expectedData);
513
+ });
514
+ ```
515
+
516
+ ## ๐Ÿ“š Examples
517
+
518
+ ### Complete Test File Example
519
+
520
+ ```typescript
521
+ import { afterEach, describe, expect, test, vi } from "vitest";
522
+
523
+ import { createMockFeedItem } from "./test-utils/fixtures";
524
+ import {
525
+ authenticateKnock,
526
+ createMockKnock,
527
+ mockNetworkSuccess,
528
+ } from "./test-utils/mocks";
529
+
530
+ describe("My Feature", () => {
531
+ afterEach(() => {
532
+ vi.clearAllMocks();
533
+ });
534
+
535
+ describe("Basic Operations", () => {
536
+ test("performs basic operation", () => {
537
+ const { knock } = createMockKnock();
538
+
539
+ expect(knock).toBeDefined();
540
+ });
541
+ });
542
+
543
+ describe("Authenticated Operations", () => {
544
+ test("performs authenticated operation", async () => {
545
+ const { knock, mockApiClient } = createMockKnock();
546
+ authenticateKnock(knock);
547
+
548
+ const expectedData = { success: true };
549
+ mockNetworkSuccess(mockApiClient, expectedData);
550
+
551
+ const result = await knock.authenticatedOperation();
552
+
553
+ expect(result).toEqual(expectedData);
554
+ });
555
+ });
556
+
557
+ describe("Data Operations", () => {
558
+ test("processes feed item", () => {
559
+ const item = createMockFeedItem({
560
+ read_at: null, // Unread item
561
+ });
562
+
563
+ const result = processItem(item);
564
+
565
+ expect(result.isUnread).toBe(true);
566
+ });
567
+ });
568
+
569
+ describe("Error Handling", () => {
570
+ test("handles errors gracefully", async () => {
571
+ const { knock, mockApiClient } = createMockKnock();
572
+
573
+ mockApiClient.makeRequest.mockRejectedValue(new Error("API Error"));
574
+
575
+ await expect(knock.faultyOperation()).rejects.toThrow("API Error");
576
+ });
577
+ });
578
+ });
579
+ ```
580
+
581
+ ## ๐ŸŽ‰ You're Ready!
582
+
583
+ With this guide and the provided utilities, you should be able to write comprehensive tests for any part of the Knock client. Remember:
584
+
585
+ 1. **Use the test utilities** - they handle the complex setup for you
586
+ 2. **Follow the patterns** - consistent structure makes tests easier to understand
587
+ 3. **Test both success and failure cases** - robust testing catches more bugs
588
+ 4. **Clean up after yourself** - prevent test pollution
589
+
590
+ Happy testing! ๐Ÿงช
package/dist/cjs/api.js DELETED
@@ -1,2 +0,0 @@
1
- "use strict";var i=Object.defineProperty;var n=(t,e,s)=>e in t?i(t,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):t[e]=s;var r=(t,e,s)=>n(t,typeof e!="symbol"?e+"":e,s);Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const u=require("axios"),l=require("axios-retry"),c=require("phoenix"),a=t=>t&&typeof t=="object"&&"default"in t?t:{default:t},p=a(u),o=a(l);class d{constructor(e){r(this,"host");r(this,"apiKey");r(this,"userToken");r(this,"axiosClient");r(this,"socket");this.host=e.host,this.apiKey=e.apiKey,this.userToken=e.userToken||null,this.axiosClient=p.default.create({baseURL:this.host,headers:{Accept:"application/json","Content-Type":"application/json",Authorization:`Bearer ${this.apiKey}`,"X-Knock-User-Token":this.userToken}}),typeof window<"u"&&(this.socket=new c.Socket(`${this.host.replace("http","ws")}/ws/v1`,{params:{user_token:this.userToken,api_key:this.apiKey}})),o.default(this.axiosClient,{retries:3,retryCondition:this.canRetryRequest,retryDelay:o.default.exponentialDelay})}async makeRequest(e){try{const s=await this.axiosClient(e);return{statusCode:s.status<300?"ok":"error",body:s.data,error:void 0,status:s.status}}catch(s){return console.error(s),{statusCode:"error",status:500,body:void 0,error:s}}}canRetryRequest(e){return o.default.isNetworkError(e)?!0:e.response?e.response.status>=500&&e.response.status<=599||e.response.status===429:!1}}exports.default=d;
2
- //# sourceMappingURL=api.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"api.js","sources":["../../src/api.ts"],"sourcesContent":["import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from \"axios\";\nimport axiosRetry from \"axios-retry\";\nimport { Socket } from \"phoenix\";\n\ntype ApiClientOptions = {\n host: string;\n apiKey: string;\n userToken: string | undefined;\n};\n\nexport interface ApiResponse {\n // eslint-disable-next-line\n error?: any;\n // eslint-disable-next-line\n body?: any;\n statusCode: \"ok\" | \"error\";\n status: number;\n}\n\nclass ApiClient {\n private host: string;\n private apiKey: string;\n private userToken: string | null;\n private axiosClient: AxiosInstance;\n\n public socket: Socket | undefined;\n\n constructor(options: ApiClientOptions) {\n this.host = options.host;\n this.apiKey = options.apiKey;\n this.userToken = options.userToken || null;\n\n // Create a retryable axios client\n this.axiosClient = axios.create({\n baseURL: this.host,\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${this.apiKey}`,\n \"X-Knock-User-Token\": this.userToken,\n },\n });\n\n if (typeof window !== \"undefined\") {\n this.socket = new Socket(`${this.host.replace(\"http\", \"ws\")}/ws/v1`, {\n params: {\n user_token: this.userToken,\n api_key: this.apiKey,\n },\n });\n }\n\n axiosRetry(this.axiosClient, {\n retries: 3,\n retryCondition: this.canRetryRequest,\n retryDelay: axiosRetry.exponentialDelay,\n });\n }\n\n async makeRequest(req: AxiosRequestConfig): Promise<ApiResponse> {\n try {\n const result = await this.axiosClient(req);\n\n return {\n statusCode: result.status < 300 ? \"ok\" : \"error\",\n body: result.data,\n error: undefined,\n status: result.status,\n };\n\n // eslint:disable-next-line\n } catch (e: unknown) {\n console.error(e);\n\n return {\n statusCode: \"error\",\n status: 500,\n body: undefined,\n error: e,\n };\n }\n }\n\n private canRetryRequest(error: AxiosError) {\n // Retry Network Errors.\n if (axiosRetry.isNetworkError(error)) {\n return true;\n }\n\n if (!error.response) {\n // Cannot determine if the request can be retried\n return false;\n }\n\n // Retry Server Errors (5xx).\n if (error.response.status >= 500 && error.response.status <= 599) {\n return true;\n }\n\n // Retry if rate limited.\n if (error.response.status === 429) {\n return true;\n }\n\n return false;\n }\n}\n\nexport default ApiClient;\n"],"names":["ApiClient","options","__publicField","axios","Socket","axiosRetry","req","result","e","error"],"mappings":"6ZAmBA,MAAMA,CAAU,CAQd,YAAYC,EAA2B,CAP/BC,EAAA,aACAA,EAAA,eACAA,EAAA,kBACAA,EAAA,oBAEDA,EAAA,eAGL,KAAK,KAAOD,EAAQ,KACpB,KAAK,OAASA,EAAQ,OACjB,KAAA,UAAYA,EAAQ,WAAa,KAGjC,KAAA,YAAcE,UAAM,OAAO,CAC9B,QAAS,KAAK,KACd,QAAS,CACP,OAAQ,mBACR,eAAgB,mBAChB,cAAe,UAAU,KAAK,MAAM,GACpC,qBAAsB,KAAK,SAAA,CAC7B,CACD,EAEG,OAAO,OAAW,MACf,KAAA,OAAS,IAAIC,EAAA,OAAO,GAAG,KAAK,KAAK,QAAQ,OAAQ,IAAI,CAAC,SAAU,CACnE,OAAQ,CACN,WAAY,KAAK,UACjB,QAAS,KAAK,MAAA,CAChB,CACD,GAGHC,EAAA,QAAW,KAAK,YAAa,CAC3B,QAAS,EACT,eAAgB,KAAK,gBACrB,WAAYA,EAAAA,QAAW,gBAAA,CACxB,CAAA,CAGH,MAAM,YAAYC,EAA+C,CAC3D,GAAA,CACF,MAAMC,EAAS,MAAM,KAAK,YAAYD,CAAG,EAElC,MAAA,CACL,WAAYC,EAAO,OAAS,IAAM,KAAO,QACzC,KAAMA,EAAO,KACb,MAAO,OACP,OAAQA,EAAO,MACjB,QAGOC,EAAY,CACnB,eAAQ,MAAMA,CAAC,EAER,CACL,WAAY,QACZ,OAAQ,IACR,KAAM,OACN,MAAOA,CACT,CAAA,CACF,CAGM,gBAAgBC,EAAmB,CAErC,OAAAJ,EAAA,QAAW,eAAeI,CAAK,EAC1B,GAGJA,EAAM,SAMPA,EAAM,SAAS,QAAU,KAAOA,EAAM,SAAS,QAAU,KAKzDA,EAAM,SAAS,SAAW,IATrB,EAaF,CAEX"}
@@ -1,2 +0,0 @@
1
- "use strict";var g=Object.defineProperty;var p=(c,e,t)=>e in c?g(c,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):c[e]=t;var d=(c,e,t)=>p(c,typeof e!="symbol"?e+"":e,t);Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const k=require("eventemitter2"),_=require("nanoid"),v=require("../../helpers.js"),m=require("../../networkStatus.js"),S=require("./socket-manager.js"),y=require("./store.js"),f=require("./utils.js"),b=c=>c&&typeof c=="object"&&"default"in c?c:{default:c},w=b(k),I={archived:"exclude"},C=2e3,U="client_";class A{constructor(e,t,s,a){d(this,"defaultOptions");d(this,"referenceId");d(this,"unsubscribeFromSocketEvents");d(this,"socketManager");d(this,"userFeedId");d(this,"broadcaster");d(this,"broadcastChannel");d(this,"disconnectTimer",null);d(this,"hasSubscribedToRealTimeUpdates",!1);d(this,"visibilityChangeHandler",()=>{});d(this,"visibilityChangeListenerConnected",!1);d(this,"store");this.knock=e,this.feedId=t,(!t||!v.isValidUuid(t))&&this.knock.log("[Feed] Invalid or missing feedId provided to the Feed constructor. The feed should be a UUID of an in-app feed channel (`in_app_feed`) found in the Knock dashboard. Please provide a valid feedId to the Feed constructor.",!0),this.feedId=t,this.userFeedId=this.buildUserFeedId(),this.referenceId=U+_.nanoid(),this.socketManager=a,this.store=y.default(),this.broadcaster=new w.default({wildcard:!0,delimiter:"."}),this.defaultOptions={...I,...f.mergeDateRangeParams(s)},this.knock.log(`[Feed] Initialized a feed on channel ${t}`),this.initializeRealtimeConnection(),this.setupBroadcastChannel()}reinitialize(e){this.socketManager=e,this.userFeedId=this.buildUserFeedId(),this.initializeRealtimeConnection(),this.setupBroadcastChannel()}teardown(){var e;this.knock.log("[Feed] Tearing down feed instance"),(e=this.socketManager)==null||e.leave(this),this.tearDownVisibilityListeners(),this.disconnectTimer&&(clearTimeout(this.disconnectTimer),this.disconnectTimer=null),this.broadcastChannel&&this.broadcastChannel.close()}dispose(){this.knock.log("[Feed] Disposing of feed instance"),this.teardown(),this.broadcaster.removeAllListeners(),this.knock.feeds.removeInstance(this)}listenForUpdates(){var e;if(this.knock.log("[Feed] Connecting to real-time service"),this.hasSubscribedToRealTimeUpdates=!0,!this.knock.isAuthenticated()){this.knock.log("[Feed] User is not authenticated, skipping listening for updates");return}this.unsubscribeFromSocketEvents=(e=this.socketManager)==null?void 0:e.join(this)}on(e,t){this.broadcaster.on(e,t)}off(e,t){this.broadcaster.off(e,t)}getState(){return this.store.getState()}async markAsSeen(e){const t=new Date().toISOString();return this.optimisticallyPerformStatusUpdate(e,"seen",{seen_at:t},"unseen_count"),this.makeStatusUpdate(e,"seen")}async markAllAsSeen(){const{metadata:e,items:t,...s}=this.store.getState();if(this.defaultOptions.status==="unseen")s.resetStore({...e,total_count:0,unseen_count:0});else{s.setMetadata({...e,unseen_count:0});const n={seen_at:new Date().toISOString()},i=t.map(o=>o.id);s.setItemAttrs(i,n)}const r=await this.makeBulkStatusUpdate("seen");return this.emitEvent("all_seen",t),r}async markAsUnseen(e){return this.optimisticallyPerformStatusUpdate(e,"unseen",{seen_at:null},"unseen_count"),this.makeStatusUpdate(e,"unseen")}async markAsRead(e){const t=new Date().toISOString();return this.optimisticallyPerformStatusUpdate(e,"read",{read_at:t},"unread_count"),this.makeStatusUpdate(e,"read")}async markAllAsRead(){const{metadata:e,items:t,...s}=this.store.getState();if(this.defaultOptions.status==="unread")s.resetStore({...e,total_count:0,unread_count:0});else{s.setMetadata({...e,unread_count:0});const n={read_at:new Date().toISOString()},i=t.map(o=>o.id);s.setItemAttrs(i,n)}const r=await this.makeBulkStatusUpdate("read");return this.emitEvent("all_read",t),r}async markAsUnread(e){return this.optimisticallyPerformStatusUpdate(e,"unread",{read_at:null},"unread_count"),this.makeStatusUpdate(e,"unread")}async markAsInteracted(e,t){const s=new Date().toISOString();return this.optimisticallyPerformStatusUpdate(e,"interacted",{read_at:s,interacted_at:s},"unread_count"),this.makeStatusUpdate(e,"interacted",t)}async markAsArchived(e){const t=this.store.getState(),s=this.defaultOptions.archived==="exclude",a=Array.isArray(e)?e:[e],r=a.map(n=>n.id);if(s){const n=a.filter(u=>!u.seen_at).length,i=a.filter(u=>!u.read_at).length,o={...t.metadata,total_count:Math.max(0,t.metadata.total_count-a.length),unseen_count:Math.max(0,t.metadata.unseen_count-n),unread_count:Math.max(0,t.metadata.unread_count-i)},l=t.items.filter(u=>!r.includes(u.id));t.setResult({entries:l,meta:o,page_info:t.pageInfo})}else t.setItemAttrs(r,{archived_at:new Date().toISOString()});return this.makeStatusUpdate(e,"archived")}async markAllAsArchived(){const{items:e,...t}=this.store.getState();if(this.defaultOptions.archived==="exclude")t.resetStore();else{const r=e.map(n=>n.id);t.setItemAttrs(r,{archived_at:new Date().toISOString()})}const a=await this.makeBulkStatusUpdate("archive");return this.emitEvent("all_archived",e),a}async markAllReadAsArchived(){const{items:e,...t}=this.store.getState(),a=e.filter(i=>i.read_at===null).map(i=>i.id);if(t.setItemAttrs(a,{archived_at:new Date().toISOString()}),this.defaultOptions.archived==="exclude"){const i=e.filter(l=>!a.includes(l.id)),o={...t.metadata,total_count:i.length,unread_count:0};t.setResult({entries:i,meta:o,page_info:t.pageInfo})}return await this.makeBulkStatusUpdate("archive")}async markAsUnarchived(e){return this.optimisticallyPerformStatusUpdate(e,"unarchived",{archived_at:null}),this.makeStatusUpdate(e,"unarchived")}async fetch(e={}){const{networkStatus:t,...s}=this.store.getState();if(!this.knock.isAuthenticated()){this.knock.log("[Feed] User is not authenticated, skipping fetch");return}if(m.isRequestInFlight(t)){this.knock.log("[Feed] Request is in flight, skipping fetch");return}s.setNetworkStatus(e.__loadingType??m.NetworkStatus.loading);const a=f.getFormattedTriggerData({...this.defaultOptions,...e}),r={...this.defaultOptions,...f.mergeDateRangeParams(e),trigger_data:a,__loadingType:void 0,__fetchSource:void 0,__experimentalCrossBrowserUpdates:void 0,auto_manage_socket_connection:void 0,auto_manage_socket_connection_delay:void 0},n=await this.knock.client().makeRequest({method:"GET",url:`/v1/users/${this.knock.userId}/feeds/${this.feedId}`,params:r});if(n.statusCode==="error"||!n.body)return s.setNetworkStatus(m.NetworkStatus.error),{status:n.statusCode,data:n.error||n.body};const i={entries:n.body.entries,meta:n.body.meta,page_info:n.body.page_info};if(e.before){const u={shouldSetPage:!1,shouldAppend:!0};s.setResult(i,u)}else if(e.after){const u={shouldSetPage:!0,shouldAppend:!0};s.setResult(i,u)}else s.setResult(i);this.broadcast("messages.new",i);const o=e.__fetchSource==="socket"?"items.received.realtime":"items.received.page",l={items:i.entries,metadata:i.meta,event:o};return this.broadcast(l.event,l),{data:i,status:n.statusCode}}async fetchNextPage(e={}){const{pageInfo:t}=this.store.getState();t.after&&this.fetch({...e,after:t.after,__loadingType:m.NetworkStatus.fetchMore})}get socketChannelTopic(){return`feeds:${this.userFeedId}`}broadcast(e,t){this.broadcaster.emit(e,t)}async onNewMessageReceived({data:e}){var n;this.knock.log("[Feed] Received new real-time message");const{items:t,...s}=this.store.getState(),a=t[0],r=(n=e[this.referenceId])==null?void 0:n.metadata;r&&s.setMetadata(r),this.fetch({before:a==null?void 0:a.__cursor,__fetchSource:"socket"})}buildUserFeedId(){return`${this.feedId}:${this.knock.userId}`}optimisticallyPerformStatusUpdate(e,t,s,a){const r=this.store.getState(),n=Array.isArray(e)?e:[e],i=n.map(o=>o.id);if(a){const{metadata:o}=r,l=n.filter(h=>{switch(t){case"seen":return h.seen_at===null;case"unseen":return h.seen_at!==null;case"read":case"interacted":return h.read_at===null;case"unread":return h.read_at!==null;default:return!0}}),u=t.startsWith("un")?l.length:-l.length;r.setMetadata({...o,[a]:Math.max(0,o[a]+u)})}r.setItemAttrs(i,s)}async makeStatusUpdate(e,t,s){const a=Array.isArray(e)?e:[e],r=a.map(i=>i.id),n=await this.knock.messages.batchUpdateStatuses(r,t,{metadata:s});return this.emitEvent(t,a),n}async makeBulkStatusUpdate(e){const t={user_ids:[this.knock.userId],engagement_status:this.defaultOptions.status!=="all"?this.defaultOptions.status:void 0,archived:this.defaultOptions.archived,has_tenant:this.defaultOptions.has_tenant,tenants:this.defaultOptions.tenant?[this.defaultOptions.tenant]:void 0};return await this.knock.messages.bulkUpdateAllStatusesInChannel({channelId:this.feedId,status:e,options:t})}setupBroadcastChannel(){this.broadcastChannel=typeof self<"u"&&"BroadcastChannel"in self?new BroadcastChannel(`knock:feed:${this.userFeedId}`):null,this.broadcastChannel&&this.defaultOptions.__experimentalCrossBrowserUpdates===!0&&(this.broadcastChannel.onmessage=e=>{switch(e.data.type){case"items:archived":case"items:unarchived":case"items:seen":case"items:unseen":case"items:read":case"items:unread":case"items:all_read":case"items:all_seen":case"items:all_archived":return this.fetch();default:return null}})}broadcastOverChannel(e,t){if(this.broadcastChannel)try{const s=JSON.parse(JSON.stringify(t));this.broadcastChannel.postMessage({type:e,payload:s})}catch(s){console.warn(`Could not broadcast ${e}, got error: ${s}`)}}initializeRealtimeConnection(){var e;this.socketManager&&(this.defaultOptions.auto_manage_socket_connection&&this.setUpVisibilityListeners(),this.hasSubscribedToRealTimeUpdates&&this.knock.isAuthenticated()&&(this.unsubscribeFromSocketEvents=(e=this.socketManager)==null?void 0:e.join(this)))}async handleSocketEvent(e){switch(e.event){case S.SocketEventType.NewMessage:this.onNewMessageReceived(e);return;default:{e.event;return}}}setUpVisibilityListeners(){typeof document>"u"||this.visibilityChangeListenerConnected||(this.visibilityChangeHandler=this.handleVisibilityChange.bind(this),this.visibilityChangeListenerConnected=!0,document.addEventListener("visibilitychange",this.visibilityChangeHandler))}tearDownVisibilityListeners(){typeof document>"u"||(document.removeEventListener("visibilitychange",this.visibilityChangeHandler),this.visibilityChangeListenerConnected=!1)}emitEvent(e,t){this.broadcaster.emit(`items.${e}`,{items:t}),this.broadcaster.emit(`items:${e}`,{items:t}),this.broadcastOverChannel(`items:${e}`,{items:t})}handleVisibilityChange(){var s,a;const e=this.defaultOptions.auto_manage_socket_connection_delay??C,t=this.knock.client();document.visibilityState==="hidden"?this.disconnectTimer=setTimeout(()=>{var r;(r=t.socket)==null||r.disconnect(),this.disconnectTimer=null},e):document.visibilityState==="visible"&&(this.disconnectTimer&&(clearTimeout(this.disconnectTimer),this.disconnectTimer=null),(s=t.socket)!=null&&s.isConnected()||(a=t.socket)==null||a.connect())}}exports.default=A;
2
- //# sourceMappingURL=feed.js.map