@hubspot/ui-extensions 0.9.2 → 0.9.4

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.
@@ -0,0 +1,396 @@
1
+ // Set up the mock before importing the module
2
+ const mockFetchAssociations = jest.fn();
3
+ const mockSelf = {
4
+ fetchAssociations: mockFetchAssociations,
5
+ };
6
+ // Mock the global self object
7
+ Object.defineProperty(global, 'self', {
8
+ value: mockSelf,
9
+ writable: true,
10
+ });
11
+ import { fetchAssociations } from '../../../experimental/crm/fetchAssociations';
12
+ describe('fetchAssociations', () => {
13
+ // Helper functions
14
+ const createMockResponse = (data, overrides = {}) => ({
15
+ ok: true,
16
+ json: jest.fn().mockResolvedValue({ data, cleanup: jest.fn() }),
17
+ ...overrides,
18
+ });
19
+ const createErrorResponse = (statusText) => ({
20
+ ok: false,
21
+ statusText,
22
+ json: jest.fn().mockResolvedValue({}),
23
+ });
24
+ const expectError = async (request, expectedMessage) => {
25
+ await expect(fetchAssociations(request)).rejects.toThrow(expectedMessage);
26
+ };
27
+ const createBasicRequest = (overrides = {}) => ({
28
+ toObjectType: '0-1',
29
+ properties: ['firstname', 'lastname'],
30
+ ...overrides,
31
+ });
32
+ beforeEach(() => {
33
+ jest.clearAllMocks();
34
+ // Set up dynamic mock that responds to request parameters
35
+ mockFetchAssociations.mockImplementation((request) => {
36
+ const { pageLength = 100, offset = 0, properties = [] } = request;
37
+ // Generate mock properties based on the request
38
+ const mockProperties = {};
39
+ properties.forEach((prop) => {
40
+ mockProperties[prop] = `Mock ${prop} (page ${Math.floor(offset / pageLength) + 1})`;
41
+ });
42
+ const mockResults = [
43
+ {
44
+ toObjectId: 1000 + offset,
45
+ associationTypes: [
46
+ { category: 'HUBSPOT_DEFINED', typeId: 1, label: 'Primary' },
47
+ ],
48
+ properties: mockProperties,
49
+ },
50
+ {
51
+ toObjectId: 1001 + offset,
52
+ associationTypes: [
53
+ {
54
+ category: 'USER_DEFINED',
55
+ typeId: 100,
56
+ label: 'Custom Association',
57
+ },
58
+ ],
59
+ properties: mockProperties,
60
+ },
61
+ ];
62
+ const mockData = {
63
+ results: mockResults,
64
+ hasMore: offset < 200,
65
+ nextOffset: offset + pageLength,
66
+ };
67
+ return Promise.resolve(createMockResponse(mockData));
68
+ });
69
+ });
70
+ describe('basic functionality', () => {
71
+ it('should return mock associations with default parameters', async () => {
72
+ const request = createBasicRequest();
73
+ const result = await fetchAssociations(request);
74
+ expect(result).toHaveProperty('data');
75
+ expect(result).toHaveProperty('cleanup');
76
+ expect(typeof result.cleanup).toBe('function');
77
+ const { data } = result;
78
+ expect(data.results).toHaveLength(2);
79
+ expect(data.hasMore).toBe(true);
80
+ expect(data.nextOffset).toBe(100);
81
+ // Check first association
82
+ expect(data.results[0]).toEqual({
83
+ toObjectId: 1000,
84
+ associationTypes: [
85
+ { category: 'HUBSPOT_DEFINED', typeId: 1, label: 'Primary' },
86
+ ],
87
+ properties: {
88
+ firstname: 'Mock firstname (page 1)',
89
+ lastname: 'Mock lastname (page 1)',
90
+ },
91
+ });
92
+ // Check second association
93
+ expect(data.results[1]).toEqual({
94
+ toObjectId: 1001,
95
+ associationTypes: [
96
+ {
97
+ category: 'USER_DEFINED',
98
+ typeId: 100,
99
+ label: 'Custom Association',
100
+ },
101
+ ],
102
+ properties: {
103
+ firstname: 'Mock firstname (page 1)',
104
+ lastname: 'Mock lastname (page 1)',
105
+ },
106
+ });
107
+ });
108
+ it('should return a cleanup function', async () => {
109
+ const request = createBasicRequest({ properties: ['firstname'] });
110
+ const result = await fetchAssociations(request);
111
+ expect(typeof result.cleanup).toBe('function');
112
+ // Should not throw when called
113
+ expect(() => result.cleanup()).not.toThrow();
114
+ });
115
+ it('should return a Promise', () => {
116
+ const request = createBasicRequest({ properties: ['firstname'] });
117
+ const result = fetchAssociations(request);
118
+ expect(result).toBeInstanceOf(Promise);
119
+ });
120
+ });
121
+ describe('pagination handling', () => {
122
+ it('should handle custom page length and offset', async () => {
123
+ const request = createBasicRequest({
124
+ properties: ['email'],
125
+ pageLength: 25,
126
+ offset: 50,
127
+ });
128
+ const result = await fetchAssociations(request);
129
+ const { data } = result;
130
+ expect(data.results).toHaveLength(2);
131
+ expect(data.nextOffset).toBe(75); // 50 + 25
132
+ expect(data.hasMore).toBe(true);
133
+ // Check that offset is reflected in object IDs
134
+ expect(data.results[0].toObjectId).toBe(1050); // 1000 + 50
135
+ expect(data.results[1].toObjectId).toBe(1051); // 1001 + 50
136
+ // Check that page number is reflected in properties
137
+ expect(data.results[0].properties.email).toBe('Mock email (page 3)'); // Math.floor(50 / 25) + 1 = 3
138
+ });
139
+ it('should calculate hasMore correctly based on offset', async () => {
140
+ // Test when there are more results
141
+ const request1 = createBasicRequest({
142
+ properties: ['firstname'],
143
+ offset: 100,
144
+ });
145
+ const result1 = await fetchAssociations(request1);
146
+ expect(result1.data.hasMore).toBe(true);
147
+ // Test when there are no more results
148
+ const request2 = createBasicRequest({
149
+ properties: ['firstname'],
150
+ offset: 200,
151
+ });
152
+ const result2 = await fetchAssociations(request2);
153
+ expect(result2.data.hasMore).toBe(false);
154
+ // Test edge case at the boundary
155
+ const request3 = createBasicRequest({
156
+ properties: ['firstname'],
157
+ offset: 199,
158
+ });
159
+ const result3 = await fetchAssociations(request3);
160
+ expect(result3.data.hasMore).toBe(true);
161
+ });
162
+ it('should generate different page numbers for different offsets', async () => {
163
+ const request1 = createBasicRequest({
164
+ properties: ['firstname'],
165
+ pageLength: 10,
166
+ offset: 0,
167
+ });
168
+ const result1 = await fetchAssociations(request1);
169
+ expect(result1.data.results[0].properties.firstname).toBe('Mock firstname (page 1)');
170
+ const request2 = createBasicRequest({
171
+ properties: ['firstname'],
172
+ pageLength: 10,
173
+ offset: 20,
174
+ });
175
+ const result2 = await fetchAssociations(request2);
176
+ expect(result2.data.results[0].properties.firstname).toBe('Mock firstname (page 3)'); // Math.floor(20 / 10) + 1 = 3
177
+ });
178
+ });
179
+ describe('properties handling', () => {
180
+ it('should handle empty properties array', async () => {
181
+ const request = createBasicRequest({ properties: [] });
182
+ const result = await fetchAssociations(request);
183
+ const { data } = result;
184
+ expect(data.results).toHaveLength(2);
185
+ expect(data.results[0].properties).toEqual({});
186
+ expect(data.results[1].properties).toEqual({});
187
+ });
188
+ it('should handle missing properties in request', async () => {
189
+ const request = createBasicRequest();
190
+ // Remove properties to test missing case
191
+ delete request.properties;
192
+ const result = await fetchAssociations(request);
193
+ const { data } = result;
194
+ expect(data.results).toHaveLength(2);
195
+ expect(data.results[0].properties).toEqual({});
196
+ expect(data.results[1].properties).toEqual({});
197
+ });
198
+ it('should handle multiple properties correctly', async () => {
199
+ const request = createBasicRequest({
200
+ properties: ['firstname', 'lastname', 'email', 'phone'],
201
+ });
202
+ const result = await fetchAssociations(request);
203
+ const { data } = result;
204
+ expect(data.results[0].properties).toEqual({
205
+ firstname: 'Mock firstname (page 1)',
206
+ lastname: 'Mock lastname (page 1)',
207
+ email: 'Mock email (page 1)',
208
+ phone: 'Mock phone (page 1)',
209
+ });
210
+ });
211
+ it('should handle different object types', async () => {
212
+ const request = createBasicRequest({
213
+ toObjectType: '0-2',
214
+ properties: ['name'],
215
+ });
216
+ const result = await fetchAssociations(request);
217
+ const { data } = result;
218
+ expect(data.results).toHaveLength(2);
219
+ expect(data.results[0].properties.name).toBe('Mock name (page 1)');
220
+ });
221
+ });
222
+ describe('error handling', () => {
223
+ it('should throw an error when response is not OK', async () => {
224
+ mockFetchAssociations.mockResolvedValueOnce(createErrorResponse('Not Found'));
225
+ const request = createBasicRequest({ properties: ['firstname'] });
226
+ await expectError(request, 'Failed to fetch associations: Not Found');
227
+ });
228
+ it('should throw an error when fetch fails', async () => {
229
+ mockFetchAssociations.mockRejectedValueOnce(new Error('Network error'));
230
+ const request = createBasicRequest({ properties: ['firstname'] });
231
+ await expectError(request, 'Network error');
232
+ });
233
+ it('should handle unknown errors', async () => {
234
+ mockFetchAssociations.mockRejectedValueOnce('Unknown error');
235
+ const request = createBasicRequest({ properties: ['firstname'] });
236
+ await expectError(request, 'Failed to fetch associations: Unknown error');
237
+ });
238
+ });
239
+ describe('response validation', () => {
240
+ it('should throw error for invalid response format - missing results', async () => {
241
+ const invalidData = { hasMore: true, nextOffset: 0 }; // missing results
242
+ mockFetchAssociations.mockResolvedValueOnce(createMockResponse(invalidData));
243
+ const request = createBasicRequest({ properties: ['firstname'] });
244
+ await expectError(request, 'Invalid response format');
245
+ });
246
+ it('should throw error for invalid response format - null data', async () => {
247
+ mockFetchAssociations.mockResolvedValueOnce(createMockResponse(null));
248
+ const request = createBasicRequest({ properties: ['firstname'] });
249
+ await expectError(request, 'Invalid response format');
250
+ });
251
+ it('should throw error for invalid association result structure', async () => {
252
+ const invalidData = {
253
+ results: [{ invalidStructure: true }],
254
+ hasMore: false,
255
+ nextOffset: 0,
256
+ };
257
+ mockFetchAssociations.mockResolvedValueOnce(createMockResponse(invalidData));
258
+ const request = createBasicRequest({ properties: ['firstname'] });
259
+ await expectError(request, 'Invalid response format');
260
+ });
261
+ it('should throw error for invalid association types array', async () => {
262
+ const invalidData = {
263
+ results: [
264
+ {
265
+ toObjectId: 1000,
266
+ associationTypes: 'not an array',
267
+ properties: {},
268
+ },
269
+ ],
270
+ hasMore: false,
271
+ nextOffset: 0,
272
+ };
273
+ mockFetchAssociations.mockResolvedValueOnce(createMockResponse(invalidData));
274
+ const request = createBasicRequest({ properties: ['firstname'] });
275
+ await expectError(request, 'Invalid response format');
276
+ });
277
+ it('should throw error for missing hasMore field', async () => {
278
+ const invalidData = {
279
+ results: [],
280
+ nextOffset: 0, // missing hasMore
281
+ };
282
+ mockFetchAssociations.mockResolvedValueOnce(createMockResponse(invalidData));
283
+ const request = createBasicRequest({ properties: ['firstname'] });
284
+ await expectError(request, 'Invalid response format');
285
+ });
286
+ });
287
+ describe('API integration', () => {
288
+ it('should call self.fetchAssociations with correct parameters', async () => {
289
+ const emptyData = { results: [], hasMore: false, nextOffset: 0 };
290
+ mockFetchAssociations.mockResolvedValueOnce(createMockResponse(emptyData));
291
+ const request = createBasicRequest({
292
+ properties: ['firstname'],
293
+ pageLength: 50,
294
+ });
295
+ await fetchAssociations(request);
296
+ expect(mockFetchAssociations).toHaveBeenCalledWith(request, undefined);
297
+ });
298
+ it('should return cleanup function from API response', async () => {
299
+ const mockCleanup = jest.fn();
300
+ const emptyData = { results: [], hasMore: false, nextOffset: 0 };
301
+ const mockApiResponse = { data: emptyData, cleanup: mockCleanup };
302
+ mockFetchAssociations.mockResolvedValueOnce({
303
+ ok: true,
304
+ json: jest.fn().mockResolvedValue(mockApiResponse),
305
+ });
306
+ const result = await fetchAssociations(createBasicRequest());
307
+ expect(result.cleanup).toBe(mockCleanup);
308
+ result.cleanup();
309
+ expect(mockCleanup).toHaveBeenCalledTimes(1);
310
+ });
311
+ it('should provide default cleanup function when none provided', async () => {
312
+ const emptyData = { results: [], hasMore: false, nextOffset: 0 };
313
+ const mockApiResponse = { data: emptyData }; // no cleanup function provided
314
+ mockFetchAssociations.mockResolvedValueOnce({
315
+ ok: true,
316
+ json: jest.fn().mockResolvedValue(mockApiResponse),
317
+ });
318
+ const result = await fetchAssociations(createBasicRequest());
319
+ expect(typeof result.cleanup).toBe('function');
320
+ expect(() => result.cleanup()).not.toThrow();
321
+ });
322
+ it('should successfully fetch associations with valid response', async () => {
323
+ const validData = {
324
+ results: [
325
+ {
326
+ toObjectId: 2000,
327
+ associationTypes: [
328
+ { category: 'HUBSPOT_DEFINED', typeId: 1, label: 'Primary' },
329
+ ],
330
+ properties: {
331
+ firstname: 'John',
332
+ lastname: 'Doe',
333
+ },
334
+ },
335
+ ],
336
+ hasMore: true,
337
+ nextOffset: 100,
338
+ };
339
+ mockFetchAssociations.mockResolvedValueOnce(createMockResponse(validData));
340
+ const request = createBasicRequest();
341
+ const result = await fetchAssociations(request);
342
+ expect(mockFetchAssociations).toHaveBeenCalledWith(request, undefined);
343
+ expect(result.data).toEqual(validData);
344
+ expect(typeof result.cleanup).toBe('function');
345
+ });
346
+ });
347
+ describe('options handling', () => {
348
+ it('should pass formatting options to the underlying fetch function', async () => {
349
+ const emptyData = { results: [], hasMore: false, nextOffset: 0 };
350
+ mockFetchAssociations.mockResolvedValueOnce(createMockResponse(emptyData));
351
+ const options = {
352
+ propertiesToFormat: ['firstname'],
353
+ formattingOptions: {
354
+ date: { format: 'MM/dd/yyyy' },
355
+ currency: { addSymbol: true },
356
+ },
357
+ };
358
+ const request = createBasicRequest();
359
+ await fetchAssociations(request, options);
360
+ expect(mockFetchAssociations).toHaveBeenCalledWith(request, options);
361
+ });
362
+ it('should handle propertiesToFormat set to "all"', async () => {
363
+ const emptyData = { results: [], hasMore: false, nextOffset: 0 };
364
+ mockFetchAssociations.mockResolvedValueOnce(createMockResponse(emptyData));
365
+ const options = {
366
+ propertiesToFormat: 'all',
367
+ formattingOptions: {
368
+ dateTime: { relative: true },
369
+ },
370
+ };
371
+ const request = createBasicRequest();
372
+ await fetchAssociations(request, options);
373
+ expect(mockFetchAssociations).toHaveBeenCalledWith(request, options);
374
+ });
375
+ it('should preserve error handling with formatting options', async () => {
376
+ mockFetchAssociations.mockRejectedValueOnce(new Error('Network error'));
377
+ const options = {
378
+ propertiesToFormat: ['firstname'],
379
+ formattingOptions: {
380
+ date: { format: 'MM/dd/yyyy' },
381
+ },
382
+ };
383
+ const request = createBasicRequest({ properties: ['firstname'] });
384
+ await expect(fetchAssociations(request, options)).rejects.toThrow('Network error');
385
+ expect(mockFetchAssociations).toHaveBeenCalledWith(request, options);
386
+ });
387
+ it('should preserve response validation with formatting options', async () => {
388
+ mockFetchAssociations.mockResolvedValueOnce(createMockResponse('Invalid response')); // data should be an object
389
+ const options = {
390
+ propertiesToFormat: 'all',
391
+ };
392
+ const request = createBasicRequest({ properties: ['firstname'] });
393
+ await expect(fetchAssociations(request, options)).rejects.toThrow('Invalid response format');
394
+ });
395
+ });
396
+ });
@@ -8,15 +8,19 @@ Object.defineProperty(global, 'self', {
8
8
  value: mockSelf,
9
9
  writable: true,
10
10
  });
11
- import { fetchCrmProperties, } from '../../../experimental/crm/fetchCrmProperties';
11
+ import { fetchCrmProperties } from '../../../experimental/crm/fetchCrmProperties';
12
+ const DEFAULT_OPTIONS = {};
12
13
  describe('fetchCrmProperties', () => {
13
14
  beforeEach(() => {
14
15
  jest.clearAllMocks();
15
16
  });
16
17
  it('successfully fetches CRM properties', async () => {
17
18
  const mockApiResponse = {
18
- firstname: 'Test value for firstname',
19
- lastname: 'Test value for lastname',
19
+ data: {
20
+ firstname: 'Test value for firstname',
21
+ lastname: 'Test value for lastname',
22
+ },
23
+ cleanup: jest.fn(),
20
24
  };
21
25
  const mockResponse = {
22
26
  ok: true,
@@ -24,42 +28,97 @@ describe('fetchCrmProperties', () => {
24
28
  };
25
29
  mockFetchCrmProperties.mockResolvedValue(mockResponse);
26
30
  const propertyNames = ['firstname', 'lastname'];
27
- const result = await fetchCrmProperties(propertyNames, jest.fn());
28
- expect(mockFetchCrmProperties).toHaveBeenCalledWith(propertyNames, expect.any(Function));
31
+ const result = await fetchCrmProperties(propertyNames, jest.fn(), DEFAULT_OPTIONS);
32
+ expect(mockFetchCrmProperties).toHaveBeenCalledWith(propertyNames, expect.any(Function), DEFAULT_OPTIONS);
29
33
  expect(result).toEqual({
30
- firstname: 'Test value for firstname',
31
- lastname: 'Test value for lastname',
34
+ data: {
35
+ firstname: 'Test value for firstname',
36
+ lastname: 'Test value for lastname',
37
+ },
38
+ cleanup: expect.any(Function),
39
+ });
40
+ });
41
+ it('successfully fetches CRM properties with null values', async () => {
42
+ const mockApiResponse = {
43
+ data: {
44
+ firstname: 'John',
45
+ lastname: null,
46
+ email: 'john@example.com',
47
+ phone: null,
48
+ },
49
+ cleanup: jest.fn(),
50
+ };
51
+ const mockResponse = {
52
+ ok: true,
53
+ json: jest.fn().mockResolvedValue(mockApiResponse),
54
+ };
55
+ mockFetchCrmProperties.mockResolvedValue(mockResponse);
56
+ const propertyNames = ['firstname', 'lastname', 'email', 'phone'];
57
+ const result = await fetchCrmProperties(propertyNames, jest.fn(), DEFAULT_OPTIONS);
58
+ expect(mockFetchCrmProperties).toHaveBeenCalledWith(propertyNames, expect.any(Function), DEFAULT_OPTIONS);
59
+ expect(result).toEqual({
60
+ data: {
61
+ firstname: 'John',
62
+ lastname: null,
63
+ email: 'john@example.com',
64
+ phone: null,
65
+ },
66
+ cleanup: expect.any(Function),
32
67
  });
33
68
  });
34
69
  it('throws an error when response is not OK', async () => {
35
70
  const mockResponse = {
36
71
  ok: false,
37
72
  statusText: 'Not Found',
73
+ json: jest.fn().mockResolvedValue({}),
38
74
  };
39
75
  mockFetchCrmProperties.mockResolvedValue(mockResponse);
40
76
  const propertyNames = ['firstname'];
41
- await expect(fetchCrmProperties(propertyNames, jest.fn())).rejects.toThrow('Failed to fetch CRM properties: Not Found');
77
+ await expect(fetchCrmProperties(propertyNames, jest.fn(), DEFAULT_OPTIONS)).rejects.toThrow('Failed to fetch CRM properties: Not Found');
42
78
  });
43
79
  it('throws an error when fetch fails', async () => {
44
80
  mockFetchCrmProperties.mockRejectedValue(new Error('Network error'));
45
81
  const propertyNames = ['firstname'];
46
- await expect(fetchCrmProperties(propertyNames, jest.fn())).rejects.toThrow('Network error');
82
+ await expect(fetchCrmProperties(propertyNames, jest.fn(), DEFAULT_OPTIONS)).rejects.toThrow('Network error');
47
83
  });
48
84
  it('throws an error if the response is not an object', async () => {
49
- const mockApiResponse = 'Invalid response';
85
+ const mockApiResponse = {
86
+ data: 'Invalid response',
87
+ cleanup: jest.fn(),
88
+ };
50
89
  const mockResponse = {
51
90
  ok: true,
52
91
  json: jest.fn().mockResolvedValue(mockApiResponse),
53
92
  };
54
93
  mockFetchCrmProperties.mockResolvedValue(mockResponse);
55
94
  const propertyNames = ['firstname'];
56
- await expect(fetchCrmProperties(propertyNames, jest.fn())).rejects.toThrow('Invalid response format');
95
+ await expect(fetchCrmProperties(propertyNames, jest.fn(), DEFAULT_OPTIONS)).rejects.toThrow('Invalid response format');
96
+ });
97
+ it('throws an error if response contains invalid property values', async () => {
98
+ const mockApiResponse = {
99
+ data: {
100
+ firstname: 'John',
101
+ lastname: 123,
102
+ email: 'john@example.com',
103
+ },
104
+ cleanup: jest.fn(),
105
+ };
106
+ const mockResponse = {
107
+ ok: true,
108
+ json: jest.fn().mockResolvedValue(mockApiResponse),
109
+ };
110
+ mockFetchCrmProperties.mockResolvedValue(mockResponse);
111
+ const propertyNames = ['firstname', 'lastname', 'email'];
112
+ await expect(fetchCrmProperties(propertyNames, jest.fn(), DEFAULT_OPTIONS)).rejects.toThrow('Invalid response format');
57
113
  });
58
114
  it('passes the propertiesUpdatedCallback and allows it to be called', async () => {
59
115
  let capturedCallback;
60
116
  const mockApiResponse = {
61
- firstname: 'Initial',
62
- lastname: 'Initial',
117
+ data: {
118
+ firstname: 'Initial',
119
+ lastname: 'Initial',
120
+ },
121
+ cleanup: jest.fn(),
63
122
  };
64
123
  const mockResponse = {
65
124
  ok: true,
@@ -71,7 +130,7 @@ describe('fetchCrmProperties', () => {
71
130
  });
72
131
  const propertyNames = ['firstname', 'lastname'];
73
132
  const mockCallback = jest.fn();
74
- await fetchCrmProperties(propertyNames, mockCallback);
133
+ await fetchCrmProperties(propertyNames, mockCallback, DEFAULT_OPTIONS);
75
134
  expect(typeof capturedCallback).toBe('function');
76
135
  // Simulate the callback being called with new properties
77
136
  const updatedProps = { firstname: 'Updated', lastname: 'Updated' };
@@ -80,4 +139,111 @@ describe('fetchCrmProperties', () => {
80
139
  }
81
140
  expect(mockCallback).toHaveBeenCalledWith(updatedProps);
82
141
  });
142
+ it('passes the propertiesUpdatedCallback and allows it to be called with null values', async () => {
143
+ let capturedCallback;
144
+ const mockApiResponse = {
145
+ data: {
146
+ firstname: 'Initial',
147
+ lastname: null,
148
+ },
149
+ cleanup: jest.fn(),
150
+ };
151
+ const mockResponse = {
152
+ ok: true,
153
+ json: jest.fn().mockResolvedValue(mockApiResponse),
154
+ };
155
+ mockFetchCrmProperties.mockImplementation((propertyNames, callback) => {
156
+ capturedCallback = callback;
157
+ return Promise.resolve(mockResponse);
158
+ });
159
+ const propertyNames = ['firstname', 'lastname'];
160
+ const mockCallback = jest.fn();
161
+ await fetchCrmProperties(propertyNames, mockCallback, DEFAULT_OPTIONS);
162
+ expect(typeof capturedCallback).toBe('function');
163
+ // Simulate the callback being called with new properties including null values
164
+ const updatedProps = { firstname: null, lastname: 'Updated Value' };
165
+ if (capturedCallback) {
166
+ capturedCallback(updatedProps);
167
+ }
168
+ expect(mockCallback).toHaveBeenCalledWith(updatedProps);
169
+ });
170
+ it('passes formatting options to the underlying fetch function', async () => {
171
+ const mockApiResponse = {
172
+ data: {
173
+ firstname: 'John',
174
+ lastname: 'Doe',
175
+ },
176
+ cleanup: jest.fn(),
177
+ };
178
+ const mockResponse = {
179
+ ok: true,
180
+ json: jest.fn().mockResolvedValue(mockApiResponse),
181
+ };
182
+ mockFetchCrmProperties.mockResolvedValue(mockResponse);
183
+ const propertyNames = ['firstname', 'lastname'];
184
+ const options = {
185
+ propertiesToFormat: ['firstname'],
186
+ formattingOptions: {
187
+ date: {
188
+ format: 'MM/dd/yyyy',
189
+ },
190
+ currency: {
191
+ addSymbol: true,
192
+ },
193
+ },
194
+ };
195
+ await fetchCrmProperties(propertyNames, jest.fn(), options);
196
+ expect(mockFetchCrmProperties).toHaveBeenCalledWith(propertyNames, expect.any(Function), options);
197
+ });
198
+ it('preserves error handling with formatting options', async () => {
199
+ mockFetchCrmProperties.mockRejectedValue(new Error('Network error'));
200
+ const propertyNames = ['firstname'];
201
+ const options = {
202
+ propertiesToFormat: ['firstname'],
203
+ formattingOptions: {
204
+ date: {
205
+ format: 'MM/dd/yyyy',
206
+ },
207
+ },
208
+ };
209
+ await expect(fetchCrmProperties(propertyNames, jest.fn(), options)).rejects.toThrow('Network error');
210
+ expect(mockFetchCrmProperties).toHaveBeenCalledWith(propertyNames, expect.any(Function), options);
211
+ });
212
+ it('preserves response validation with formatting options', async () => {
213
+ const mockApiResponse = {
214
+ data: 'Invalid response',
215
+ cleanup: jest.fn(),
216
+ };
217
+ const mockResponse = {
218
+ ok: true,
219
+ json: jest.fn().mockResolvedValue(mockApiResponse),
220
+ };
221
+ mockFetchCrmProperties.mockResolvedValue(mockResponse);
222
+ const propertyNames = ['firstname'];
223
+ const options = {
224
+ propertiesToFormat: 'all',
225
+ };
226
+ await expect(fetchCrmProperties(propertyNames, jest.fn(), options)).rejects.toThrow('Invalid response format');
227
+ });
228
+ it('returns cleanup function that can be called', async () => {
229
+ const mockCleanup = jest.fn();
230
+ const mockApiResponse = {
231
+ data: {
232
+ firstname: 'John',
233
+ },
234
+ cleanup: mockCleanup,
235
+ };
236
+ const mockResponse = {
237
+ ok: true,
238
+ json: jest.fn().mockResolvedValue(mockApiResponse),
239
+ };
240
+ mockFetchCrmProperties.mockResolvedValue(mockResponse);
241
+ const propertyNames = ['firstname'];
242
+ const result = await fetchCrmProperties(propertyNames, jest.fn(), DEFAULT_OPTIONS);
243
+ expect(result.cleanup).toBe(mockCleanup);
244
+ expect(typeof result.cleanup).toBe('function');
245
+ // Verify cleanup can be called without errors
246
+ result.cleanup();
247
+ expect(mockCleanup).toHaveBeenCalledTimes(1);
248
+ });
83
249
  });