@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.
- package/dist/__tests__/experimental/crm/fetchAssociations.spec.d.ts +1 -0
- package/dist/__tests__/experimental/crm/fetchAssociations.spec.js +396 -0
- package/dist/__tests__/experimental/crm/fetchCrmProperties.spec.js +180 -14
- package/dist/__tests__/experimental/hooks/useAssociations.spec.d.ts +1 -0
- package/dist/__tests__/experimental/hooks/useAssociations.spec.js +368 -0
- package/dist/__tests__/experimental/hooks/useCrmProperties.spec.js +151 -4
- package/dist/experimental/crm/fetchAssociations.d.ts +29 -0
- package/dist/experimental/crm/fetchAssociations.js +42 -0
- package/dist/experimental/crm/fetchCrmProperties.d.ts +23 -2
- package/dist/experimental/crm/fetchCrmProperties.js +22 -16
- package/dist/experimental/hooks/useAssociations.d.ts +10 -0
- package/dist/experimental/hooks/useAssociations.js +116 -0
- package/dist/experimental/hooks/useCrmProperties.d.ts +3 -2
- package/dist/experimental/hooks/useCrmProperties.js +95 -31
- package/dist/experimental/index.d.ts +34 -1
- package/dist/experimental/index.js +19 -4
- package/dist/experimental/types.d.ts +212 -6
- package/dist/types.d.ts +15 -1
- package/dist/types.js +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
2
|
+
import { useAssociations } from '../../../experimental/hooks/useAssociations';
|
|
3
|
+
// Mock the logger module
|
|
4
|
+
jest.mock('../../../logger', () => ({
|
|
5
|
+
logger: {
|
|
6
|
+
debug: jest.fn(),
|
|
7
|
+
info: jest.fn(),
|
|
8
|
+
warn: jest.fn(),
|
|
9
|
+
error: jest.fn(),
|
|
10
|
+
},
|
|
11
|
+
}));
|
|
12
|
+
// Mock the fetchAssociations module
|
|
13
|
+
jest.mock('../../../experimental/crm/fetchAssociations');
|
|
14
|
+
const mockFetchAssociations = jest.fn();
|
|
15
|
+
import * as fetchAssociationsModule from '../../../experimental/crm/fetchAssociations';
|
|
16
|
+
fetchAssociationsModule.fetchAssociations = mockFetchAssociations;
|
|
17
|
+
describe('useAssociations', () => {
|
|
18
|
+
let originalError;
|
|
19
|
+
beforeAll(() => {
|
|
20
|
+
// Suppress React act() warning coming from @testing-library/react
|
|
21
|
+
originalError = console.error;
|
|
22
|
+
console.error = (...args) => {
|
|
23
|
+
if (args[0]?.includes('ReactDOMTestUtils.act'))
|
|
24
|
+
return;
|
|
25
|
+
originalError.call(console, ...args);
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
// Reset the mock before each test
|
|
30
|
+
mockFetchAssociations.mockReset();
|
|
31
|
+
});
|
|
32
|
+
afterAll(() => {
|
|
33
|
+
console.error = originalError;
|
|
34
|
+
});
|
|
35
|
+
describe('initial state', () => {
|
|
36
|
+
it('should return the initial values for results, error, isLoading, hasMore, and nextOffset', async () => {
|
|
37
|
+
const { result } = renderHook(() => useAssociations({
|
|
38
|
+
toObjectType: '0-1',
|
|
39
|
+
properties: ['firstname', 'lastname'],
|
|
40
|
+
pageLength: 25,
|
|
41
|
+
offset: 0,
|
|
42
|
+
}));
|
|
43
|
+
await waitFor(() => {
|
|
44
|
+
expect(result.current.results).toEqual([]);
|
|
45
|
+
expect(result.current.error).toBeNull();
|
|
46
|
+
expect(result.current.isLoading).toBe(true);
|
|
47
|
+
expect(result.current.hasMore).toBe(false);
|
|
48
|
+
expect(result.current.nextOffset).toBe(0);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('successful data fetching', () => {
|
|
53
|
+
it('should successfully fetch and return associations', async () => {
|
|
54
|
+
mockFetchAssociations.mockResolvedValue({
|
|
55
|
+
data: {
|
|
56
|
+
results: [
|
|
57
|
+
{
|
|
58
|
+
toObjectId: 1001,
|
|
59
|
+
associationTypes: [
|
|
60
|
+
{ category: 'HUBSPOT_DEFINED', typeId: 1, label: 'Primary' },
|
|
61
|
+
],
|
|
62
|
+
properties: {
|
|
63
|
+
firstname: 'John',
|
|
64
|
+
lastname: 'Doe',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
toObjectId: 1002,
|
|
69
|
+
associationTypes: [
|
|
70
|
+
{ category: 'USER_DEFINED', typeId: 100, label: 'Custom' },
|
|
71
|
+
],
|
|
72
|
+
properties: {
|
|
73
|
+
firstname: 'Jane',
|
|
74
|
+
lastname: 'Smith',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
hasMore: true,
|
|
79
|
+
nextOffset: 25,
|
|
80
|
+
},
|
|
81
|
+
cleanup: jest.fn(),
|
|
82
|
+
});
|
|
83
|
+
const request = {
|
|
84
|
+
toObjectType: '0-1',
|
|
85
|
+
properties: ['firstname', 'lastname'],
|
|
86
|
+
pageLength: 25,
|
|
87
|
+
offset: 0,
|
|
88
|
+
};
|
|
89
|
+
const { result } = renderHook(() => useAssociations(request));
|
|
90
|
+
await waitFor(() => {
|
|
91
|
+
expect(result.current.results).toEqual([
|
|
92
|
+
{
|
|
93
|
+
toObjectId: 1001,
|
|
94
|
+
associationTypes: [
|
|
95
|
+
{ category: 'HUBSPOT_DEFINED', typeId: 1, label: 'Primary' },
|
|
96
|
+
],
|
|
97
|
+
properties: {
|
|
98
|
+
firstname: 'John',
|
|
99
|
+
lastname: 'Doe',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
toObjectId: 1002,
|
|
104
|
+
associationTypes: [
|
|
105
|
+
{ category: 'USER_DEFINED', typeId: 100, label: 'Custom' },
|
|
106
|
+
],
|
|
107
|
+
properties: {
|
|
108
|
+
firstname: 'Jane',
|
|
109
|
+
lastname: 'Smith',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
113
|
+
expect(result.current.error).toBeNull();
|
|
114
|
+
expect(result.current.isLoading).toBe(false);
|
|
115
|
+
expect(result.current.hasMore).toBe(true);
|
|
116
|
+
expect(result.current.nextOffset).toBe(25);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
it('should handle empty results correctly', async () => {
|
|
120
|
+
mockFetchAssociations.mockResolvedValue({
|
|
121
|
+
data: {
|
|
122
|
+
results: [],
|
|
123
|
+
hasMore: false,
|
|
124
|
+
nextOffset: 0,
|
|
125
|
+
},
|
|
126
|
+
cleanup: jest.fn(),
|
|
127
|
+
});
|
|
128
|
+
const request = {
|
|
129
|
+
toObjectType: '0-1',
|
|
130
|
+
properties: ['firstname'],
|
|
131
|
+
pageLength: 25,
|
|
132
|
+
offset: 0,
|
|
133
|
+
};
|
|
134
|
+
const { result } = renderHook(() => useAssociations(request));
|
|
135
|
+
await waitFor(() => {
|
|
136
|
+
expect(result.current.results).toEqual([]);
|
|
137
|
+
expect(result.current.hasMore).toBe(false);
|
|
138
|
+
expect(result.current.nextOffset).toBe(0);
|
|
139
|
+
expect(result.current.isLoading).toBe(false);
|
|
140
|
+
expect(result.current.error).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
it('should handle different object types correctly', async () => {
|
|
144
|
+
mockFetchAssociations.mockResolvedValue({
|
|
145
|
+
data: {
|
|
146
|
+
results: [
|
|
147
|
+
{
|
|
148
|
+
toObjectId: 2001,
|
|
149
|
+
associationTypes: [
|
|
150
|
+
{
|
|
151
|
+
category: 'HUBSPOT_DEFINED',
|
|
152
|
+
typeId: 2,
|
|
153
|
+
label: 'Company Primary',
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
properties: {
|
|
157
|
+
name: 'Test Company',
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
hasMore: false,
|
|
162
|
+
nextOffset: 25,
|
|
163
|
+
},
|
|
164
|
+
cleanup: jest.fn(),
|
|
165
|
+
});
|
|
166
|
+
const request = {
|
|
167
|
+
toObjectType: '0-2',
|
|
168
|
+
properties: ['name'],
|
|
169
|
+
pageLength: 25,
|
|
170
|
+
offset: 0,
|
|
171
|
+
};
|
|
172
|
+
const { result } = renderHook(() => useAssociations(request));
|
|
173
|
+
await waitFor(() => {
|
|
174
|
+
expect(result.current.results).toEqual([
|
|
175
|
+
{
|
|
176
|
+
toObjectId: 2001,
|
|
177
|
+
associationTypes: [
|
|
178
|
+
{
|
|
179
|
+
category: 'HUBSPOT_DEFINED',
|
|
180
|
+
typeId: 2,
|
|
181
|
+
label: 'Company Primary',
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
properties: {
|
|
185
|
+
name: 'Test Company',
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
]);
|
|
189
|
+
expect(result.current.isLoading).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
expect(mockFetchAssociations).toHaveBeenCalledWith(expect.objectContaining({
|
|
192
|
+
toObjectType: '0-2',
|
|
193
|
+
}), {});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
describe('error handling', () => {
|
|
197
|
+
it('should handle fetch errors correctly', async () => {
|
|
198
|
+
const errorMessage = 'Failed to fetch associations';
|
|
199
|
+
mockFetchAssociations.mockRejectedValue(new Error(errorMessage));
|
|
200
|
+
const request = {
|
|
201
|
+
toObjectType: '0-1',
|
|
202
|
+
properties: ['firstname'],
|
|
203
|
+
pageLength: 25,
|
|
204
|
+
offset: 0,
|
|
205
|
+
};
|
|
206
|
+
const { result } = renderHook(() => useAssociations(request));
|
|
207
|
+
await waitFor(() => {
|
|
208
|
+
expect(result.current.error).toBeInstanceOf(Error);
|
|
209
|
+
expect(result.current.error?.message).toBe(errorMessage);
|
|
210
|
+
expect(result.current.results).toEqual([]);
|
|
211
|
+
expect(result.current.isLoading).toBe(false);
|
|
212
|
+
expect(result.current.hasMore).toBe(false);
|
|
213
|
+
expect(result.current.nextOffset).toBe(0);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
describe('pagination handling', () => {
|
|
218
|
+
it('should handle pagination correctly', async () => {
|
|
219
|
+
mockFetchAssociations.mockResolvedValue({
|
|
220
|
+
data: {
|
|
221
|
+
results: [
|
|
222
|
+
{
|
|
223
|
+
toObjectId: 1025,
|
|
224
|
+
associationTypes: [
|
|
225
|
+
{ category: 'HUBSPOT_DEFINED', typeId: 1, label: 'Primary' },
|
|
226
|
+
],
|
|
227
|
+
properties: {
|
|
228
|
+
firstname: 'Page 2 User',
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
hasMore: false,
|
|
233
|
+
nextOffset: 50,
|
|
234
|
+
},
|
|
235
|
+
cleanup: jest.fn(),
|
|
236
|
+
});
|
|
237
|
+
const request = {
|
|
238
|
+
toObjectType: '0-1',
|
|
239
|
+
properties: ['firstname'],
|
|
240
|
+
pageLength: 25,
|
|
241
|
+
offset: 25, // Second page
|
|
242
|
+
};
|
|
243
|
+
const { result } = renderHook(() => useAssociations(request));
|
|
244
|
+
await waitFor(() => {
|
|
245
|
+
expect(result.current.results).toEqual([
|
|
246
|
+
{
|
|
247
|
+
toObjectId: 1025,
|
|
248
|
+
associationTypes: [
|
|
249
|
+
{ category: 'HUBSPOT_DEFINED', typeId: 1, label: 'Primary' },
|
|
250
|
+
],
|
|
251
|
+
properties: {
|
|
252
|
+
firstname: 'Page 2 User',
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
]);
|
|
256
|
+
expect(result.current.hasMore).toBe(false);
|
|
257
|
+
expect(result.current.nextOffset).toBe(50);
|
|
258
|
+
expect(result.current.isLoading).toBe(false);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
describe('options handling', () => {
|
|
263
|
+
it('should pass formatting options to fetchAssociations', async () => {
|
|
264
|
+
mockFetchAssociations.mockResolvedValue({
|
|
265
|
+
data: {
|
|
266
|
+
results: [],
|
|
267
|
+
hasMore: false,
|
|
268
|
+
nextOffset: 0,
|
|
269
|
+
},
|
|
270
|
+
cleanup: jest.fn(),
|
|
271
|
+
});
|
|
272
|
+
const request = {
|
|
273
|
+
toObjectType: '0-1',
|
|
274
|
+
properties: ['firstname', 'lastname'],
|
|
275
|
+
pageLength: 25,
|
|
276
|
+
offset: 0,
|
|
277
|
+
};
|
|
278
|
+
const options = {
|
|
279
|
+
propertiesToFormat: ['firstname'],
|
|
280
|
+
formattingOptions: {
|
|
281
|
+
date: {
|
|
282
|
+
format: 'MM/DD/YYYY',
|
|
283
|
+
relative: true,
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
renderHook(() => useAssociations(request, options));
|
|
288
|
+
await waitFor(() => {
|
|
289
|
+
expect(mockFetchAssociations).toHaveBeenCalledWith(request, options);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
it('should use default empty options when no options provided', async () => {
|
|
293
|
+
mockFetchAssociations.mockResolvedValue({
|
|
294
|
+
data: {
|
|
295
|
+
results: [],
|
|
296
|
+
hasMore: false,
|
|
297
|
+
nextOffset: 0,
|
|
298
|
+
},
|
|
299
|
+
cleanup: jest.fn(),
|
|
300
|
+
});
|
|
301
|
+
const request = {
|
|
302
|
+
toObjectType: '0-1',
|
|
303
|
+
properties: ['firstname'],
|
|
304
|
+
pageLength: 25,
|
|
305
|
+
offset: 0,
|
|
306
|
+
};
|
|
307
|
+
renderHook(() => useAssociations(request));
|
|
308
|
+
await waitFor(() => {
|
|
309
|
+
expect(mockFetchAssociations).toHaveBeenCalledWith(request, {});
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
describe('lifecycle management', () => {
|
|
314
|
+
it('should call cleanup function on unmount', async () => {
|
|
315
|
+
const mockCleanup = jest.fn();
|
|
316
|
+
mockFetchAssociations.mockResolvedValue({
|
|
317
|
+
data: {
|
|
318
|
+
results: [],
|
|
319
|
+
hasMore: false,
|
|
320
|
+
nextOffset: 0,
|
|
321
|
+
},
|
|
322
|
+
cleanup: mockCleanup,
|
|
323
|
+
});
|
|
324
|
+
const request = {
|
|
325
|
+
toObjectType: '0-1',
|
|
326
|
+
properties: ['firstname'],
|
|
327
|
+
pageLength: 25,
|
|
328
|
+
offset: 0,
|
|
329
|
+
};
|
|
330
|
+
const { unmount } = renderHook(() => useAssociations(request));
|
|
331
|
+
await waitFor(() => {
|
|
332
|
+
expect(mockFetchAssociations).toHaveBeenCalled();
|
|
333
|
+
});
|
|
334
|
+
unmount();
|
|
335
|
+
await waitFor(() => {
|
|
336
|
+
expect(mockCleanup).toHaveBeenCalled();
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
it('should handle stable reference optimization for request', async () => {
|
|
340
|
+
mockFetchAssociations.mockResolvedValue({
|
|
341
|
+
data: {
|
|
342
|
+
results: [],
|
|
343
|
+
hasMore: false,
|
|
344
|
+
nextOffset: 0,
|
|
345
|
+
},
|
|
346
|
+
cleanup: jest.fn(),
|
|
347
|
+
});
|
|
348
|
+
const request = {
|
|
349
|
+
toObjectType: '0-1',
|
|
350
|
+
properties: ['firstname'],
|
|
351
|
+
pageLength: 25,
|
|
352
|
+
offset: 0,
|
|
353
|
+
};
|
|
354
|
+
const { rerender } = renderHook(({ requestProp }) => useAssociations(requestProp), {
|
|
355
|
+
initialProps: { requestProp: request },
|
|
356
|
+
});
|
|
357
|
+
await waitFor(() => {
|
|
358
|
+
expect(mockFetchAssociations).toHaveBeenCalledTimes(1);
|
|
359
|
+
});
|
|
360
|
+
// Rerender with the same request object content but different reference
|
|
361
|
+
rerender({ requestProp: { ...request } });
|
|
362
|
+
await waitFor(() => {
|
|
363
|
+
// Should not call fetchAssociations again due to stable reference optimization
|
|
364
|
+
expect(mockFetchAssociations).toHaveBeenCalledTimes(1);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
});
|
|
@@ -42,8 +42,11 @@ describe('useCrmProperties', () => {
|
|
|
42
42
|
});
|
|
43
43
|
it('should successfully fetch and return CRM properties', async () => {
|
|
44
44
|
mockFetchCrmProperties.mockResolvedValue({
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
data: {
|
|
46
|
+
firstname: 'Test value for firstname',
|
|
47
|
+
lastname: 'Test value for lastname',
|
|
48
|
+
},
|
|
49
|
+
cleanup: jest.fn(),
|
|
47
50
|
});
|
|
48
51
|
const propertyNames = ['firstname', 'lastname'];
|
|
49
52
|
const { result } = renderHook(() => useCrmProperties(propertyNames));
|
|
@@ -73,7 +76,10 @@ describe('useCrmProperties', () => {
|
|
|
73
76
|
let capturedCallback;
|
|
74
77
|
mockFetchCrmProperties.mockImplementation((_propertyNames, propertiesUpdatedCallback) => {
|
|
75
78
|
capturedCallback = propertiesUpdatedCallback;
|
|
76
|
-
return {
|
|
79
|
+
return {
|
|
80
|
+
data: { firstname: 'Initial', lastname: 'Initial' },
|
|
81
|
+
cleanup: jest.fn(),
|
|
82
|
+
};
|
|
77
83
|
});
|
|
78
84
|
const propertyNames = ['firstname', 'lastname'];
|
|
79
85
|
const { result } = renderHook(() => useCrmProperties(propertyNames));
|
|
@@ -87,9 +93,150 @@ describe('useCrmProperties', () => {
|
|
|
87
93
|
const updatedProperties = { firstname: 'Updated', lastname: 'Updated' };
|
|
88
94
|
await waitFor(() => {
|
|
89
95
|
if (capturedCallback) {
|
|
90
|
-
capturedCallback(updatedProperties);
|
|
96
|
+
capturedCallback?.(updatedProperties);
|
|
97
|
+
expect(result.current.properties).toEqual(updatedProperties);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
it('should update properties when propertiesUpdatedCallback is called with null values', async () => {
|
|
102
|
+
// Capture the callback so we can simulate an external update to CRM properties
|
|
103
|
+
let capturedCallback;
|
|
104
|
+
mockFetchCrmProperties.mockImplementation((_propertyNames, propertiesUpdatedCallback) => {
|
|
105
|
+
capturedCallback = propertiesUpdatedCallback;
|
|
106
|
+
return {
|
|
107
|
+
data: { firstname: 'Initial', lastname: null },
|
|
108
|
+
cleanup: jest.fn(),
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
const propertyNames = ['firstname', 'lastname'];
|
|
112
|
+
const { result } = renderHook(() => useCrmProperties(propertyNames));
|
|
113
|
+
await waitFor(() => {
|
|
114
|
+
expect(result.current.properties).toEqual({
|
|
115
|
+
firstname: 'Initial',
|
|
116
|
+
lastname: null,
|
|
117
|
+
});
|
|
118
|
+
expect(result.current.isLoading).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
const updatedProperties = { firstname: null, lastname: 'Updated Value' };
|
|
121
|
+
await waitFor(() => {
|
|
122
|
+
if (capturedCallback) {
|
|
123
|
+
capturedCallback?.(updatedProperties);
|
|
91
124
|
expect(result.current.properties).toEqual(updatedProperties);
|
|
92
125
|
}
|
|
93
126
|
});
|
|
94
127
|
});
|
|
128
|
+
// This will become "should pass formatting options to fetchCrmProperties" in the next PR
|
|
129
|
+
it('should pass formatting options to fetchCrmProperties', async () => {
|
|
130
|
+
mockFetchCrmProperties.mockResolvedValue({
|
|
131
|
+
data: {
|
|
132
|
+
firstname: 'John',
|
|
133
|
+
lastname: 'Doe',
|
|
134
|
+
},
|
|
135
|
+
cleanup: jest.fn(),
|
|
136
|
+
});
|
|
137
|
+
const propertyNames = ['firstname', 'lastname'];
|
|
138
|
+
const options = {
|
|
139
|
+
propertiesToFormat: ['firstname'],
|
|
140
|
+
};
|
|
141
|
+
renderHook(() => useCrmProperties(propertyNames, options));
|
|
142
|
+
await waitFor(() => {
|
|
143
|
+
expect(mockFetchCrmProperties).toHaveBeenCalledWith(propertyNames, expect.any(Function), options);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
it('should use default empty options when no options provided', async () => {
|
|
147
|
+
mockFetchCrmProperties.mockResolvedValue({
|
|
148
|
+
data: {
|
|
149
|
+
firstname: 'John',
|
|
150
|
+
},
|
|
151
|
+
cleanup: jest.fn(),
|
|
152
|
+
});
|
|
153
|
+
const propertyNames = ['firstname'];
|
|
154
|
+
const defaultOptions = {};
|
|
155
|
+
renderHook(() => useCrmProperties(propertyNames));
|
|
156
|
+
await waitFor(() => {
|
|
157
|
+
expect(mockFetchCrmProperties).toHaveBeenCalledWith(['firstname'], expect.any(Function), defaultOptions);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
it('should not re-fetch when options object reference changes but content is the same', async () => {
|
|
161
|
+
mockFetchCrmProperties.mockResolvedValue({
|
|
162
|
+
data: {
|
|
163
|
+
firstname: 'John',
|
|
164
|
+
},
|
|
165
|
+
cleanup: jest.fn(),
|
|
166
|
+
});
|
|
167
|
+
const propertyNames = ['firstname'];
|
|
168
|
+
const initialOptions = { propertiesToFormat: ['firstname'] };
|
|
169
|
+
const { rerender } = renderHook(({ options }) => useCrmProperties(propertyNames, options), { initialProps: { options: initialOptions } });
|
|
170
|
+
await waitFor(() => {
|
|
171
|
+
expect(mockFetchCrmProperties).toHaveBeenCalledTimes(1);
|
|
172
|
+
});
|
|
173
|
+
const newOptionsWithSameContent = { propertiesToFormat: ['firstname'] };
|
|
174
|
+
rerender({ options: newOptionsWithSameContent });
|
|
175
|
+
await waitFor(() => {
|
|
176
|
+
expect(mockFetchCrmProperties).toHaveBeenCalledTimes(1);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
it('should re-fetch when options content actually changes', async () => {
|
|
180
|
+
mockFetchCrmProperties.mockResolvedValue({
|
|
181
|
+
data: {
|
|
182
|
+
firstname: 'John',
|
|
183
|
+
},
|
|
184
|
+
cleanup: jest.fn(),
|
|
185
|
+
});
|
|
186
|
+
const propertyNames = ['firstname'];
|
|
187
|
+
const initialOptions = { propertiesToFormat: ['firstname'] };
|
|
188
|
+
const { rerender } = renderHook(({ options }) => useCrmProperties(propertyNames, options), { initialProps: { options: initialOptions } });
|
|
189
|
+
await waitFor(() => {
|
|
190
|
+
expect(mockFetchCrmProperties).toHaveBeenCalledTimes(1);
|
|
191
|
+
expect(mockFetchCrmProperties).toHaveBeenLastCalledWith(['firstname'], expect.any(Function), initialOptions);
|
|
192
|
+
});
|
|
193
|
+
const newOptions = {
|
|
194
|
+
propertiesToFormat: ['firstname'],
|
|
195
|
+
formattingOptions: { dateFormat: 'yyyy-MM-dd' },
|
|
196
|
+
};
|
|
197
|
+
rerender({ options: newOptions });
|
|
198
|
+
await waitFor(() => {
|
|
199
|
+
expect(mockFetchCrmProperties).toHaveBeenCalledTimes(2);
|
|
200
|
+
expect(mockFetchCrmProperties).toHaveBeenLastCalledWith(['firstname'], expect.any(Function), newOptions);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
it('should call cleanup function when component unmounts', async () => {
|
|
204
|
+
const mockCleanup = jest.fn();
|
|
205
|
+
mockFetchCrmProperties.mockResolvedValue({
|
|
206
|
+
data: {
|
|
207
|
+
firstname: 'John',
|
|
208
|
+
},
|
|
209
|
+
cleanup: mockCleanup,
|
|
210
|
+
});
|
|
211
|
+
const propertyNames = ['firstname'];
|
|
212
|
+
const { unmount } = renderHook(() => useCrmProperties(propertyNames));
|
|
213
|
+
await waitFor(() => {
|
|
214
|
+
expect(mockFetchCrmProperties).toHaveBeenCalledTimes(1);
|
|
215
|
+
});
|
|
216
|
+
unmount();
|
|
217
|
+
expect(mockCleanup).toHaveBeenCalledTimes(1);
|
|
218
|
+
});
|
|
219
|
+
it('should call cleanup function when dependencies change', async () => {
|
|
220
|
+
const mockCleanup1 = jest.fn();
|
|
221
|
+
const mockCleanup2 = jest.fn();
|
|
222
|
+
mockFetchCrmProperties
|
|
223
|
+
.mockResolvedValueOnce({
|
|
224
|
+
data: { firstname: 'John' },
|
|
225
|
+
cleanup: mockCleanup1,
|
|
226
|
+
})
|
|
227
|
+
.mockResolvedValueOnce({
|
|
228
|
+
data: { firstname: 'Jane' },
|
|
229
|
+
cleanup: mockCleanup2,
|
|
230
|
+
});
|
|
231
|
+
const { rerender } = renderHook(({ propertyNames }) => useCrmProperties(propertyNames), { initialProps: { propertyNames: ['firstname'] } });
|
|
232
|
+
await waitFor(() => {
|
|
233
|
+
expect(mockFetchCrmProperties).toHaveBeenCalledTimes(1);
|
|
234
|
+
});
|
|
235
|
+
// Change dependencies to trigger cleanup
|
|
236
|
+
rerender({ propertyNames: ['lastname'] });
|
|
237
|
+
await waitFor(() => {
|
|
238
|
+
expect(mockFetchCrmProperties).toHaveBeenCalledTimes(2);
|
|
239
|
+
expect(mockCleanup1).toHaveBeenCalledTimes(1);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
95
242
|
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { FetchCrmPropertiesOptions } from './fetchCrmProperties';
|
|
2
|
+
export type AssociationResult = {
|
|
3
|
+
toObjectId: number;
|
|
4
|
+
associationTypes: {
|
|
5
|
+
category: string;
|
|
6
|
+
typeId: number;
|
|
7
|
+
label: string;
|
|
8
|
+
}[];
|
|
9
|
+
properties: {
|
|
10
|
+
[key: string]: string | null;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export type AssociationsResponse = {
|
|
14
|
+
results: AssociationResult[];
|
|
15
|
+
hasMore: boolean;
|
|
16
|
+
nextOffset: number;
|
|
17
|
+
};
|
|
18
|
+
export type FetchAssociationsRequest = {
|
|
19
|
+
toObjectType: string;
|
|
20
|
+
properties?: string[];
|
|
21
|
+
pageLength?: number;
|
|
22
|
+
offset?: number;
|
|
23
|
+
};
|
|
24
|
+
export interface FetchAssociationsResult {
|
|
25
|
+
data: AssociationsResponse;
|
|
26
|
+
error?: string;
|
|
27
|
+
cleanup: () => void;
|
|
28
|
+
}
|
|
29
|
+
export declare const fetchAssociations: (request: FetchAssociationsRequest, options?: FetchCrmPropertiesOptions) => Promise<FetchAssociationsResult>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
function isAssociationsResponse(data) {
|
|
2
|
+
if (data === null ||
|
|
3
|
+
typeof data !== 'object' ||
|
|
4
|
+
!Array.isArray(data.results) ||
|
|
5
|
+
typeof data.hasMore !== 'boolean' ||
|
|
6
|
+
typeof data.nextOffset !== 'number') {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
return data.results.every((result) => result !== null &&
|
|
10
|
+
typeof result === 'object' &&
|
|
11
|
+
typeof result.toObjectId === 'number' &&
|
|
12
|
+
Array.isArray(result.associationTypes) &&
|
|
13
|
+
result.properties !== null &&
|
|
14
|
+
typeof result.properties === 'object');
|
|
15
|
+
}
|
|
16
|
+
export const fetchAssociations = async (request, options) => {
|
|
17
|
+
let response;
|
|
18
|
+
let result;
|
|
19
|
+
try {
|
|
20
|
+
// eslint-disable-next-line hubspot-dev/no-confusing-browser-globals
|
|
21
|
+
response = await self.fetchAssociations(request, options);
|
|
22
|
+
result = await response.json();
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
throw error instanceof Error
|
|
26
|
+
? error
|
|
27
|
+
: new Error('Failed to fetch associations: Unknown error');
|
|
28
|
+
}
|
|
29
|
+
if (result.error) {
|
|
30
|
+
throw new Error(result.error);
|
|
31
|
+
}
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
throw new Error(`Failed to fetch associations: ${response.statusText}`);
|
|
34
|
+
}
|
|
35
|
+
if (!isAssociationsResponse(result.data)) {
|
|
36
|
+
throw new Error('Invalid response format');
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
data: result.data,
|
|
40
|
+
cleanup: result.cleanup || (() => { }),
|
|
41
|
+
};
|
|
42
|
+
};
|
|
@@ -1,4 +1,25 @@
|
|
|
1
1
|
export type CrmPropertiesResponse = {
|
|
2
|
-
[key: string]: string;
|
|
2
|
+
[key: string]: string | null;
|
|
3
3
|
};
|
|
4
|
-
export
|
|
4
|
+
export type FetchCrmPropertiesOptions = {
|
|
5
|
+
propertiesToFormat?: string[] | 'all';
|
|
6
|
+
formattingOptions?: {
|
|
7
|
+
date?: {
|
|
8
|
+
format?: string;
|
|
9
|
+
relative?: boolean;
|
|
10
|
+
};
|
|
11
|
+
dateTime?: {
|
|
12
|
+
format?: string;
|
|
13
|
+
relative?: boolean;
|
|
14
|
+
};
|
|
15
|
+
currency?: {
|
|
16
|
+
addSymbol?: boolean;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export interface FetchCrmPropertiesResult {
|
|
21
|
+
data: Record<string, string | null>;
|
|
22
|
+
error?: string;
|
|
23
|
+
cleanup: () => void;
|
|
24
|
+
}
|
|
25
|
+
export declare const fetchCrmProperties: (propertyNames: string[], propertiesUpdatedCallback: (properties: Record<string, string | null>) => void, options?: FetchCrmPropertiesOptions) => Promise<FetchCrmPropertiesResult>;
|