@hubspot/ui-extensions 0.13.0 → 0.13.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/dist/__generated__/version.d.ts +2 -0
- package/dist/__generated__/version.js +4 -0
- package/dist/__tests__/crm/hooks/useAssociations.spec.js +51 -0
- package/dist/__tests__/crm/utils/fetchAssociations.spec.js +0 -10
- package/dist/__tests__/experimental/hooks/useCrmSearch.spec.d.ts +1 -0
- package/dist/__tests__/experimental/hooks/useCrmSearch.spec.js +586 -0
- package/dist/__tests__/experimental/hooks/utils/fetchCrmSearch.spec.d.ts +1 -0
- package/dist/__tests__/experimental/hooks/utils/fetchCrmSearch.spec.js +217 -0
- package/dist/crm/hooks/useAssociations.d.ts +1 -1
- package/dist/crm/hooks/useAssociations.js +14 -5
- package/dist/crm/utils/fetchAssociations.js +0 -3
- package/dist/experimental/hooks/useCrmSearch.d.ts +2 -0
- package/dist/experimental/hooks/useCrmSearch.js +206 -0
- package/dist/experimental/hooks/utils/fetchCrmSearch.d.ts +6 -0
- package/dist/experimental/hooks/utils/fetchCrmSearch.js +39 -0
- package/dist/experimental/index.d.ts +1 -0
- package/dist/experimental/index.js +1 -0
- package/dist/shared/types/components/table.d.ts +16 -0
- package/dist/shared/types/context.d.ts +1 -0
- package/dist/shared/types/experimental.d.ts +1 -1
- package/dist/shared/types/hooks.d.ts +72 -0
- package/dist/shared/types/hooks.js +1 -0
- package/dist/shared/types/worker-globals.d.ts +5 -0
- package/dist/testing/internal/mocks/mock-hooks.js +26 -0
- package/dist/testing/internal/types-internal.d.ts +2 -0
- package/dist/testing/types.d.ts +10 -0
- package/dist/utils/pagination.d.ts +0 -9
- package/package.json +2 -1
|
@@ -93,6 +93,57 @@ describe('useAssociations with Pagination', () => {
|
|
|
93
93
|
expect(result.current.pagination.pageSize).toBe(10); // DEFAULT_PAGE_SIZE
|
|
94
94
|
});
|
|
95
95
|
});
|
|
96
|
+
it('should reflect updated pageLength in pagination.pageSize when config changes', async () => {
|
|
97
|
+
mockFetchAssociations.mockResolvedValue({
|
|
98
|
+
data: {
|
|
99
|
+
results: [],
|
|
100
|
+
hasMore: false,
|
|
101
|
+
nextOffset: 0,
|
|
102
|
+
},
|
|
103
|
+
cleanup: vi.fn(),
|
|
104
|
+
});
|
|
105
|
+
const { result, rerender } = renderHook(({ pageLength }) => useAssociations({
|
|
106
|
+
toObjectType: '0-1',
|
|
107
|
+
properties: ['firstname'],
|
|
108
|
+
pageLength,
|
|
109
|
+
}), { initialProps: { pageLength: 10 } });
|
|
110
|
+
await waitFor(() => {
|
|
111
|
+
expect(result.current.pagination.pageSize).toBe(10);
|
|
112
|
+
});
|
|
113
|
+
rerender({ pageLength: 25 });
|
|
114
|
+
await waitFor(() => {
|
|
115
|
+
expect(result.current.pagination.pageSize).toBe(25);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
it('should reset pagination to first page when pageLength changes mid-session', async () => {
|
|
119
|
+
mockFetchAssociations
|
|
120
|
+
.mockResolvedValueOnce(mockResponse([createAssociation(1)], true, 10))
|
|
121
|
+
.mockResolvedValueOnce(mockResponse([createAssociation(2)], true, 20))
|
|
122
|
+
.mockResolvedValueOnce(mockResponse([createAssociation(3)], true, 25));
|
|
123
|
+
const { result, rerender } = renderHook(({ pageLength }) => useAssociations({
|
|
124
|
+
toObjectType: '0-1',
|
|
125
|
+
pageLength,
|
|
126
|
+
}), { initialProps: { pageLength: 10 } });
|
|
127
|
+
await waitFor(() => {
|
|
128
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
129
|
+
expect(result.current.pagination.hasNextPage).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
result.current.pagination.nextPage();
|
|
132
|
+
await waitFor(() => {
|
|
133
|
+
expect(result.current.pagination.currentPage).toBe(2);
|
|
134
|
+
expect(result.current.pagination.hasPreviousPage).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
rerender({ pageLength: 25 });
|
|
137
|
+
await waitFor(() => {
|
|
138
|
+
expect(result.current.pagination.pageSize).toBe(25);
|
|
139
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
140
|
+
expect(result.current.pagination.hasPreviousPage).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
expect(mockFetchAssociations).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
143
|
+
pageLength: 25,
|
|
144
|
+
offset: undefined,
|
|
145
|
+
}), expect.any(Object));
|
|
146
|
+
});
|
|
96
147
|
});
|
|
97
148
|
describe('data fetching', () => {
|
|
98
149
|
it('should successfully fetch and return associations with pagination info', async () => {
|
|
@@ -18,11 +18,6 @@ describe('fetchAssociations', () => {
|
|
|
18
18
|
json: vi.fn().mockResolvedValue({ data, cleanup: vi.fn() }),
|
|
19
19
|
...overrides,
|
|
20
20
|
});
|
|
21
|
-
const createErrorResponse = (statusText) => ({
|
|
22
|
-
ok: false,
|
|
23
|
-
statusText,
|
|
24
|
-
json: vi.fn().mockResolvedValue({}),
|
|
25
|
-
});
|
|
26
21
|
const expectError = async (request, expectedMessage) => {
|
|
27
22
|
await expect(fetchAssociations(request)).rejects.toThrow(expectedMessage);
|
|
28
23
|
};
|
|
@@ -222,11 +217,6 @@ describe('fetchAssociations', () => {
|
|
|
222
217
|
});
|
|
223
218
|
});
|
|
224
219
|
describe('error handling', () => {
|
|
225
|
-
it('should throw an error when response is not OK', async () => {
|
|
226
|
-
mockFetchAssociations.mockResolvedValueOnce(createErrorResponse('Not Found'));
|
|
227
|
-
const request = createBasicRequest({ properties: ['firstname'] });
|
|
228
|
-
await expectError(request, 'Failed to fetch associations: Not Found');
|
|
229
|
-
});
|
|
230
220
|
it('should throw an error when fetch fails', async () => {
|
|
231
221
|
mockFetchAssociations.mockRejectedValueOnce(new Error('Network error'));
|
|
232
222
|
const request = createBasicRequest({ properties: ['firstname'] });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
2
|
+
import { vi } from 'vitest';
|
|
3
|
+
import { useCrmSearch } from "../../../experimental/hooks/useCrmSearch.js";
|
|
4
|
+
import { fetchCrmSearch } from "../../../experimental/hooks/utils/fetchCrmSearch.js";
|
|
5
|
+
vi.mock('../../../logger', () => ({
|
|
6
|
+
logger: {
|
|
7
|
+
debug: vi.fn(),
|
|
8
|
+
info: vi.fn(),
|
|
9
|
+
warn: vi.fn(),
|
|
10
|
+
error: vi.fn(),
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
vi.mock('../../../experimental/hooks/utils/fetchCrmSearch.ts', async (importOriginal) => {
|
|
14
|
+
const actual = (await importOriginal());
|
|
15
|
+
return {
|
|
16
|
+
...actual,
|
|
17
|
+
fetchCrmSearch: vi.fn(),
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
const mockFetchCrmSearch = fetchCrmSearch;
|
|
21
|
+
const createSearchResult = (objectId) => ({
|
|
22
|
+
objectId,
|
|
23
|
+
properties: {},
|
|
24
|
+
});
|
|
25
|
+
const mockResponse = (results, { hasMore = false, total = results.length, after, } = {}) => ({
|
|
26
|
+
data: { results, total, hasMore, after },
|
|
27
|
+
cleanup: vi.fn(),
|
|
28
|
+
});
|
|
29
|
+
describe('useCrmSearch', () => {
|
|
30
|
+
let originalError;
|
|
31
|
+
beforeAll(() => {
|
|
32
|
+
originalError = console.error;
|
|
33
|
+
console.error = (...args) => {
|
|
34
|
+
if (typeof args[0] === 'string' &&
|
|
35
|
+
(args[0].includes('ReactDOMTestUtils.act') ||
|
|
36
|
+
args[0].includes('was not wrapped in act')))
|
|
37
|
+
return;
|
|
38
|
+
originalError.call(console, ...args);
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
mockFetchCrmSearch.mockReset();
|
|
43
|
+
});
|
|
44
|
+
afterAll(() => {
|
|
45
|
+
console.error = originalError;
|
|
46
|
+
});
|
|
47
|
+
describe('initial state', () => {
|
|
48
|
+
it('should initialize with proper pagination state', async () => {
|
|
49
|
+
mockFetchCrmSearch.mockResolvedValue(mockResponse([]));
|
|
50
|
+
const { result } = renderHook(() => useCrmSearch({
|
|
51
|
+
objectType: '0-1',
|
|
52
|
+
properties: ['firstname', 'lastname'],
|
|
53
|
+
pageSize: 10,
|
|
54
|
+
}));
|
|
55
|
+
await waitFor(() => {
|
|
56
|
+
expect(result.current.results).toEqual([]);
|
|
57
|
+
expect(result.current.total).toBe(0);
|
|
58
|
+
expect(result.current.error).toBeNull();
|
|
59
|
+
expect(result.current.isLoading).toBe(false);
|
|
60
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
61
|
+
expect(result.current.pagination.pageSize).toBe(10);
|
|
62
|
+
expect(result.current.pagination.hasNextPage).toBe(false);
|
|
63
|
+
expect(result.current.pagination.hasPreviousPage).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
it('should use default page size when not specified', async () => {
|
|
67
|
+
mockFetchCrmSearch.mockResolvedValue(mockResponse([]));
|
|
68
|
+
const { result } = renderHook(() => useCrmSearch({
|
|
69
|
+
objectType: '0-1',
|
|
70
|
+
properties: ['firstname'],
|
|
71
|
+
}));
|
|
72
|
+
await waitFor(() => {
|
|
73
|
+
expect(result.current.pagination.pageSize).toBe(10); // DEFAULT_PAGE_SIZE
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
it('should reflect updated pageSize in pagination.pageSize when config changes', async () => {
|
|
77
|
+
mockFetchCrmSearch.mockResolvedValue(mockResponse([]));
|
|
78
|
+
const { result, rerender } = renderHook(({ pageSize }) => useCrmSearch({
|
|
79
|
+
objectType: '0-1',
|
|
80
|
+
properties: ['firstname'],
|
|
81
|
+
pageSize,
|
|
82
|
+
}), { initialProps: { pageSize: 10 } });
|
|
83
|
+
await waitFor(() => {
|
|
84
|
+
expect(result.current.pagination.pageSize).toBe(10);
|
|
85
|
+
});
|
|
86
|
+
rerender({ pageSize: 25 });
|
|
87
|
+
await waitFor(() => {
|
|
88
|
+
expect(result.current.pagination.pageSize).toBe(25);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
it('should reset pagination to first page when pageSize changes mid-session', async () => {
|
|
92
|
+
mockFetchCrmSearch
|
|
93
|
+
.mockResolvedValueOnce(mockResponse([createSearchResult(1)], {
|
|
94
|
+
hasMore: true,
|
|
95
|
+
total: 3,
|
|
96
|
+
after: 'cursor-1',
|
|
97
|
+
}))
|
|
98
|
+
.mockResolvedValueOnce(mockResponse([createSearchResult(2)], {
|
|
99
|
+
hasMore: true,
|
|
100
|
+
total: 3,
|
|
101
|
+
after: 'cursor-2',
|
|
102
|
+
}))
|
|
103
|
+
.mockResolvedValueOnce(mockResponse([createSearchResult(3)], { hasMore: true, total: 3 }));
|
|
104
|
+
const { result, rerender } = renderHook(({ pageSize }) => useCrmSearch({
|
|
105
|
+
objectType: '0-1',
|
|
106
|
+
pageSize,
|
|
107
|
+
}), { initialProps: { pageSize: 10 } });
|
|
108
|
+
await waitFor(() => {
|
|
109
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
110
|
+
expect(result.current.pagination.hasNextPage).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
result.current.pagination.nextPage();
|
|
113
|
+
await waitFor(() => {
|
|
114
|
+
expect(result.current.pagination.currentPage).toBe(2);
|
|
115
|
+
expect(result.current.pagination.hasPreviousPage).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
rerender({ pageSize: 25 });
|
|
118
|
+
await waitFor(() => {
|
|
119
|
+
expect(result.current.pagination.pageSize).toBe(25);
|
|
120
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
121
|
+
expect(result.current.pagination.hasPreviousPage).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
expect(mockFetchCrmSearch).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
124
|
+
pageSize: 25,
|
|
125
|
+
after: undefined,
|
|
126
|
+
}), expect.any(Object));
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
describe('data fetching', () => {
|
|
130
|
+
it('should successfully fetch and return CRM search results with pagination info', async () => {
|
|
131
|
+
mockFetchCrmSearch.mockResolvedValue({
|
|
132
|
+
data: {
|
|
133
|
+
results: [
|
|
134
|
+
{
|
|
135
|
+
objectId: 1001,
|
|
136
|
+
properties: { firstname: 'John', lastname: 'Doe' },
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
total: 42,
|
|
140
|
+
hasMore: true,
|
|
141
|
+
after: 'next-cursor',
|
|
142
|
+
},
|
|
143
|
+
cleanup: vi.fn(),
|
|
144
|
+
});
|
|
145
|
+
const { result } = renderHook(() => useCrmSearch({
|
|
146
|
+
objectType: '0-1',
|
|
147
|
+
properties: ['firstname', 'lastname'],
|
|
148
|
+
pageSize: 10,
|
|
149
|
+
}));
|
|
150
|
+
await waitFor(() => {
|
|
151
|
+
expect(result.current.results).toHaveLength(1);
|
|
152
|
+
expect(result.current.results[0].objectId).toBe(1001);
|
|
153
|
+
expect(result.current.total).toBe(42);
|
|
154
|
+
expect(result.current.pagination.hasNextPage).toBe(true);
|
|
155
|
+
expect(result.current.pagination.hasPreviousPage).toBe(false);
|
|
156
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
157
|
+
expect(result.current.isLoading).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
expect(mockFetchCrmSearch).toHaveBeenCalledWith(expect.objectContaining({
|
|
160
|
+
objectType: '0-1',
|
|
161
|
+
properties: ['firstname', 'lastname'],
|
|
162
|
+
pageSize: 10,
|
|
163
|
+
after: undefined,
|
|
164
|
+
}), expect.any(Object));
|
|
165
|
+
});
|
|
166
|
+
it('should handle empty results correctly', async () => {
|
|
167
|
+
mockFetchCrmSearch.mockResolvedValue(mockResponse([]));
|
|
168
|
+
const { result } = renderHook(() => useCrmSearch({
|
|
169
|
+
objectType: '0-1',
|
|
170
|
+
properties: ['firstname'],
|
|
171
|
+
pageSize: 5,
|
|
172
|
+
}));
|
|
173
|
+
await waitFor(() => {
|
|
174
|
+
expect(result.current.results).toEqual([]);
|
|
175
|
+
expect(result.current.total).toBe(0);
|
|
176
|
+
expect(result.current.pagination.hasNextPage).toBe(false);
|
|
177
|
+
expect(result.current.pagination.hasPreviousPage).toBe(false);
|
|
178
|
+
expect(result.current.isLoading).toBe(false);
|
|
179
|
+
expect(result.current.error).toBeNull();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
it('should pass query, filterGroups, and sorts through to fetchCrmSearch', async () => {
|
|
183
|
+
mockFetchCrmSearch.mockResolvedValue(mockResponse([]));
|
|
184
|
+
const config = {
|
|
185
|
+
objectType: '0-1',
|
|
186
|
+
properties: ['firstname'],
|
|
187
|
+
query: 'john',
|
|
188
|
+
filterGroups: [
|
|
189
|
+
{
|
|
190
|
+
filters: [
|
|
191
|
+
{
|
|
192
|
+
propertyName: 'lifecyclestage',
|
|
193
|
+
operator: 'EQ',
|
|
194
|
+
value: 'lead',
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
sorts: [
|
|
200
|
+
{ propertyName: 'createdate', direction: 'DESCENDING' },
|
|
201
|
+
],
|
|
202
|
+
pageSize: 10,
|
|
203
|
+
};
|
|
204
|
+
renderHook(() => useCrmSearch(config));
|
|
205
|
+
await waitFor(() => {
|
|
206
|
+
expect(mockFetchCrmSearch).toHaveBeenCalledWith(expect.objectContaining({
|
|
207
|
+
objectType: '0-1',
|
|
208
|
+
properties: ['firstname'],
|
|
209
|
+
query: 'john',
|
|
210
|
+
filterGroups: config.filterGroups,
|
|
211
|
+
sorts: config.sorts,
|
|
212
|
+
pageSize: 10,
|
|
213
|
+
after: undefined,
|
|
214
|
+
}), expect.any(Object));
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
describe('pagination actions', () => {
|
|
219
|
+
it('should navigate to next page using the after cursor', async () => {
|
|
220
|
+
mockFetchCrmSearch.mockResolvedValueOnce(mockResponse([createSearchResult(1)], {
|
|
221
|
+
hasMore: true,
|
|
222
|
+
total: 2,
|
|
223
|
+
after: 'cursor-page-2',
|
|
224
|
+
}));
|
|
225
|
+
mockFetchCrmSearch.mockResolvedValueOnce(mockResponse([createSearchResult(2)], { hasMore: false, total: 2 }));
|
|
226
|
+
const { result } = renderHook(() => useCrmSearch({ objectType: '0-1', pageSize: 10 }));
|
|
227
|
+
await waitFor(() => {
|
|
228
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
229
|
+
expect(result.current.pagination.hasNextPage).toBe(true);
|
|
230
|
+
});
|
|
231
|
+
result.current.pagination.nextPage();
|
|
232
|
+
await waitFor(() => {
|
|
233
|
+
expect(result.current.pagination.currentPage).toBe(2);
|
|
234
|
+
expect(result.current.pagination.hasNextPage).toBe(false);
|
|
235
|
+
expect(result.current.pagination.hasPreviousPage).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
expect(mockFetchCrmSearch).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
238
|
+
after: 'cursor-page-2',
|
|
239
|
+
}), expect.any(Object));
|
|
240
|
+
});
|
|
241
|
+
it('should navigate to previous page using the stored cursor history', async () => {
|
|
242
|
+
mockFetchCrmSearch.mockResolvedValueOnce(mockResponse([createSearchResult(1)], {
|
|
243
|
+
hasMore: true,
|
|
244
|
+
total: 3,
|
|
245
|
+
after: 'cursor-page-2',
|
|
246
|
+
}));
|
|
247
|
+
mockFetchCrmSearch.mockResolvedValueOnce(mockResponse([createSearchResult(2)], {
|
|
248
|
+
hasMore: true,
|
|
249
|
+
total: 3,
|
|
250
|
+
after: 'cursor-page-3',
|
|
251
|
+
}));
|
|
252
|
+
mockFetchCrmSearch.mockResolvedValueOnce(mockResponse([createSearchResult(1)], {
|
|
253
|
+
hasMore: true,
|
|
254
|
+
total: 3,
|
|
255
|
+
after: 'cursor-page-2',
|
|
256
|
+
}));
|
|
257
|
+
const { result } = renderHook(() => useCrmSearch({ objectType: '0-1', pageSize: 10 }));
|
|
258
|
+
await waitFor(() => {
|
|
259
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
260
|
+
expect(result.current.pagination.hasNextPage).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
result.current.pagination.nextPage();
|
|
263
|
+
await waitFor(() => {
|
|
264
|
+
expect(result.current.pagination.currentPage).toBe(2);
|
|
265
|
+
expect(result.current.pagination.hasPreviousPage).toBe(true);
|
|
266
|
+
});
|
|
267
|
+
result.current.pagination.previousPage();
|
|
268
|
+
await waitFor(() => {
|
|
269
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
270
|
+
expect(result.current.pagination.hasPreviousPage).toBe(false);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
it('should reset to the first page correctly', async () => {
|
|
274
|
+
mockFetchCrmSearch.mockResolvedValue(mockResponse([], { hasMore: true, total: 30, after: 'next' }));
|
|
275
|
+
const { result } = renderHook(() => useCrmSearch({ objectType: '0-1', pageSize: 10 }));
|
|
276
|
+
await waitFor(() => {
|
|
277
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
278
|
+
expect(result.current.pagination.hasNextPage).toBe(true);
|
|
279
|
+
});
|
|
280
|
+
result.current.pagination.nextPage();
|
|
281
|
+
await waitFor(() => {
|
|
282
|
+
expect(result.current.pagination.currentPage).toBe(2);
|
|
283
|
+
});
|
|
284
|
+
result.current.pagination.nextPage();
|
|
285
|
+
await waitFor(() => {
|
|
286
|
+
expect(result.current.pagination.currentPage).toBe(3);
|
|
287
|
+
});
|
|
288
|
+
result.current.pagination.reset();
|
|
289
|
+
await waitFor(() => {
|
|
290
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
291
|
+
expect(result.current.pagination.hasPreviousPage).toBe(false);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
it('should not refetch when reset is called on the first page', async () => {
|
|
295
|
+
mockFetchCrmSearch.mockResolvedValueOnce(mockResponse([createSearchResult(1)], {
|
|
296
|
+
hasMore: true,
|
|
297
|
+
total: 2,
|
|
298
|
+
after: 'next',
|
|
299
|
+
}));
|
|
300
|
+
const { result } = renderHook(() => useCrmSearch({ objectType: '0-1', pageSize: 10 }));
|
|
301
|
+
await waitFor(() => {
|
|
302
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
303
|
+
});
|
|
304
|
+
result.current.pagination.reset();
|
|
305
|
+
await waitFor(() => {
|
|
306
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
307
|
+
});
|
|
308
|
+
expect(result.current.results).toEqual([createSearchResult(1)]);
|
|
309
|
+
expect(mockFetchCrmSearch).toHaveBeenCalledTimes(1);
|
|
310
|
+
});
|
|
311
|
+
it('should not allow navigation beyond pagination boundaries', async () => {
|
|
312
|
+
mockFetchCrmSearch.mockResolvedValue(mockResponse([], { hasMore: false, total: 0 }));
|
|
313
|
+
const { result } = renderHook(() => useCrmSearch({ objectType: '0-1', pageSize: 10 }));
|
|
314
|
+
await waitFor(() => {
|
|
315
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
316
|
+
expect(result.current.pagination.hasNextPage).toBe(false);
|
|
317
|
+
});
|
|
318
|
+
result.current.pagination.nextPage();
|
|
319
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
320
|
+
result.current.pagination.previousPage();
|
|
321
|
+
expect(result.current.pagination.currentPage).toBe(1);
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
describe('error handling', () => {
|
|
325
|
+
it('should handle fetch errors correctly', async () => {
|
|
326
|
+
const errorMessage = 'Failed to fetch CRM search results';
|
|
327
|
+
mockFetchCrmSearch.mockRejectedValue(new Error(errorMessage));
|
|
328
|
+
const { result } = renderHook(() => useCrmSearch({
|
|
329
|
+
objectType: '0-1',
|
|
330
|
+
properties: ['firstname'],
|
|
331
|
+
pageSize: 10,
|
|
332
|
+
}));
|
|
333
|
+
await waitFor(() => {
|
|
334
|
+
expect(result.current.error).toBeInstanceOf(Error);
|
|
335
|
+
expect(result.current.error?.message).toBe(errorMessage);
|
|
336
|
+
expect(result.current.results).toEqual([]);
|
|
337
|
+
expect(result.current.total).toBe(0);
|
|
338
|
+
expect(result.current.isLoading).toBe(false);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
it('should handle null config gracefully', async () => {
|
|
342
|
+
mockFetchCrmSearch.mockResolvedValue(mockResponse([]));
|
|
343
|
+
// @ts-expect-error - intentional type issue to check the failure state
|
|
344
|
+
const { result } = renderHook(() => useCrmSearch(null));
|
|
345
|
+
await waitFor(() => {
|
|
346
|
+
expect(result.current.isLoading).toBe(false);
|
|
347
|
+
expect(result.current.results).toEqual([]);
|
|
348
|
+
expect(result.current.pagination.pageSize).toBe(10); // DEFAULT_PAGE_SIZE
|
|
349
|
+
});
|
|
350
|
+
expect(mockFetchCrmSearch).toHaveBeenCalledWith(expect.objectContaining({
|
|
351
|
+
objectType: undefined,
|
|
352
|
+
properties: undefined,
|
|
353
|
+
query: undefined,
|
|
354
|
+
filterGroups: undefined,
|
|
355
|
+
sorts: undefined,
|
|
356
|
+
pageSize: 10,
|
|
357
|
+
after: undefined,
|
|
358
|
+
}), expect.any(Object));
|
|
359
|
+
});
|
|
360
|
+
it('should handle undefined config gracefully', async () => {
|
|
361
|
+
mockFetchCrmSearch.mockResolvedValue(mockResponse([]));
|
|
362
|
+
// @ts-expect-error - intentional type issue to check the failure state
|
|
363
|
+
const { result } = renderHook(() => useCrmSearch(undefined));
|
|
364
|
+
await waitFor(() => {
|
|
365
|
+
expect(result.current.isLoading).toBe(false);
|
|
366
|
+
expect(result.current.results).toEqual([]);
|
|
367
|
+
expect(result.current.pagination.pageSize).toBe(10); // DEFAULT_PAGE_SIZE
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
it('should handle empty config object gracefully', async () => {
|
|
371
|
+
mockFetchCrmSearch.mockResolvedValue(mockResponse([]));
|
|
372
|
+
// @ts-expect-error - intentional type issue to check the failure state
|
|
373
|
+
const { result } = renderHook(() => useCrmSearch({}));
|
|
374
|
+
await waitFor(() => {
|
|
375
|
+
expect(result.current.isLoading).toBe(false);
|
|
376
|
+
expect(result.current.results).toEqual([]);
|
|
377
|
+
expect(result.current.pagination.pageSize).toBe(10); // DEFAULT_PAGE_SIZE
|
|
378
|
+
});
|
|
379
|
+
expect(mockFetchCrmSearch).toHaveBeenCalledWith(expect.objectContaining({
|
|
380
|
+
objectType: undefined,
|
|
381
|
+
pageSize: 10,
|
|
382
|
+
after: undefined,
|
|
383
|
+
}), expect.any(Object));
|
|
384
|
+
});
|
|
385
|
+
it('should handle config missing objectType', async () => {
|
|
386
|
+
mockFetchCrmSearch.mockResolvedValue(mockResponse([]));
|
|
387
|
+
const { result } = renderHook(() =>
|
|
388
|
+
// @ts-expect-error - intentional type issue to check the failure state
|
|
389
|
+
useCrmSearch({ properties: ['firstname'], pageSize: 5 }));
|
|
390
|
+
await waitFor(() => {
|
|
391
|
+
expect(result.current.isLoading).toBe(false);
|
|
392
|
+
expect(result.current.pagination.pageSize).toBe(5);
|
|
393
|
+
});
|
|
394
|
+
expect(mockFetchCrmSearch).toHaveBeenCalledWith(expect.objectContaining({
|
|
395
|
+
objectType: undefined,
|
|
396
|
+
properties: ['firstname'],
|
|
397
|
+
pageSize: 5,
|
|
398
|
+
after: undefined,
|
|
399
|
+
}), expect.any(Object));
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
describe('options handling', () => {
|
|
403
|
+
it('should pass formatting options to fetchCrmSearch', async () => {
|
|
404
|
+
mockFetchCrmSearch.mockResolvedValue(mockResponse([]));
|
|
405
|
+
const config = {
|
|
406
|
+
objectType: '0-1',
|
|
407
|
+
properties: ['firstname', 'lastname'],
|
|
408
|
+
pageSize: 10,
|
|
409
|
+
};
|
|
410
|
+
const options = {
|
|
411
|
+
propertiesToFormat: ['firstname'],
|
|
412
|
+
formattingOptions: {
|
|
413
|
+
date: { format: 'MM/DD/YYYY', relative: true },
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
renderHook(() => useCrmSearch(config, options));
|
|
417
|
+
await waitFor(() => {
|
|
418
|
+
expect(mockFetchCrmSearch).toHaveBeenCalledWith(expect.objectContaining(config), options);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
it('should use default empty options when no options provided', async () => {
|
|
422
|
+
mockFetchCrmSearch.mockResolvedValue(mockResponse([]));
|
|
423
|
+
const config = {
|
|
424
|
+
objectType: '0-1',
|
|
425
|
+
properties: ['firstname'],
|
|
426
|
+
pageSize: 10,
|
|
427
|
+
};
|
|
428
|
+
renderHook(() => useCrmSearch(config));
|
|
429
|
+
await waitFor(() => {
|
|
430
|
+
expect(mockFetchCrmSearch).toHaveBeenCalledWith(expect.objectContaining(config), {
|
|
431
|
+
propertiesToFormat: undefined,
|
|
432
|
+
formattingOptions: undefined,
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
describe('refetch', () => {
|
|
438
|
+
it('should handle complete refetch lifecycle with loading states and data preservation', async () => {
|
|
439
|
+
const initialData = [createSearchResult(1)];
|
|
440
|
+
const refetchedData = [createSearchResult(2)];
|
|
441
|
+
let resolveRefetch = () => { };
|
|
442
|
+
const refetchPromise = new Promise((resolve) => {
|
|
443
|
+
resolveRefetch = resolve;
|
|
444
|
+
});
|
|
445
|
+
mockFetchCrmSearch
|
|
446
|
+
.mockResolvedValueOnce(mockResponse(initialData))
|
|
447
|
+
.mockImplementationOnce(() => refetchPromise);
|
|
448
|
+
const { result } = renderHook(() => useCrmSearch({ objectType: '0-1', pageSize: 10 }));
|
|
449
|
+
await waitFor(() => {
|
|
450
|
+
expect(result.current.results).toEqual(initialData);
|
|
451
|
+
expect(result.current.isLoading).toBe(false);
|
|
452
|
+
expect(result.current.isRefetching).toBe(false);
|
|
453
|
+
});
|
|
454
|
+
const refetchCall = result.current.refetch();
|
|
455
|
+
await waitFor(() => expect(result.current.isRefetching).toBe(true));
|
|
456
|
+
expect(result.current.results).toEqual(initialData);
|
|
457
|
+
resolveRefetch(mockResponse(refetchedData, { hasMore: true, total: 5, after: 'next' }));
|
|
458
|
+
await refetchCall;
|
|
459
|
+
await waitFor(() => {
|
|
460
|
+
expect(result.current.isRefetching).toBe(false);
|
|
461
|
+
expect(result.current.results).toEqual(refetchedData);
|
|
462
|
+
expect(result.current.total).toBe(5);
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
it('should handle errors during refetch and allow recovery', async () => {
|
|
466
|
+
const initialData = [createSearchResult(1)];
|
|
467
|
+
const recoveredData = [createSearchResult(2)];
|
|
468
|
+
mockFetchCrmSearch
|
|
469
|
+
.mockResolvedValueOnce(mockResponse(initialData))
|
|
470
|
+
.mockRejectedValueOnce(new Error('Failed to refetch CRM search results'))
|
|
471
|
+
.mockResolvedValueOnce(mockResponse(recoveredData));
|
|
472
|
+
const { result } = renderHook(() => useCrmSearch({ objectType: '0-1', pageSize: 10 }));
|
|
473
|
+
await waitFor(() => expect(result.current.results).toEqual(initialData));
|
|
474
|
+
await result.current.refetch();
|
|
475
|
+
await waitFor(() => {
|
|
476
|
+
expect(result.current.error?.message).toBe('Failed to refetch CRM search results');
|
|
477
|
+
expect(result.current.results).toEqual(initialData);
|
|
478
|
+
});
|
|
479
|
+
await result.current.refetch();
|
|
480
|
+
await waitFor(() => {
|
|
481
|
+
expect(result.current.error).toBeNull();
|
|
482
|
+
expect(result.current.results).toEqual(recoveredData);
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
it('should cancel in-flight refetch when called again', async () => {
|
|
486
|
+
const initialData = [createSearchResult(1)];
|
|
487
|
+
const firstRefetchCleanup = vi.fn();
|
|
488
|
+
const secondRefetchCleanup = vi.fn();
|
|
489
|
+
mockFetchCrmSearch
|
|
490
|
+
.mockResolvedValueOnce(mockResponse(initialData))
|
|
491
|
+
.mockResolvedValueOnce({
|
|
492
|
+
...mockResponse([createSearchResult(2)]),
|
|
493
|
+
cleanup: firstRefetchCleanup,
|
|
494
|
+
})
|
|
495
|
+
.mockResolvedValueOnce({
|
|
496
|
+
...mockResponse([createSearchResult(3)]),
|
|
497
|
+
cleanup: secondRefetchCleanup,
|
|
498
|
+
});
|
|
499
|
+
const { result } = renderHook(() => useCrmSearch({ objectType: '0-1', pageSize: 10 }));
|
|
500
|
+
await waitFor(() => expect(result.current.results).toEqual(initialData));
|
|
501
|
+
await Promise.all([result.current.refetch(), result.current.refetch()]);
|
|
502
|
+
await waitFor(() => expect(result.current.results).toEqual([createSearchResult(3)]));
|
|
503
|
+
expect(firstRefetchCleanup).toHaveBeenCalledTimes(1);
|
|
504
|
+
expect(secondRefetchCleanup).not.toHaveBeenCalled();
|
|
505
|
+
});
|
|
506
|
+
it('should maintain current page and pagination state during refetch', async () => {
|
|
507
|
+
mockFetchCrmSearch
|
|
508
|
+
.mockResolvedValueOnce(mockResponse([createSearchResult(1)], {
|
|
509
|
+
hasMore: true,
|
|
510
|
+
total: 2,
|
|
511
|
+
after: 'cursor-2',
|
|
512
|
+
}))
|
|
513
|
+
.mockResolvedValueOnce(mockResponse([createSearchResult(2)], {
|
|
514
|
+
hasMore: true,
|
|
515
|
+
total: 2,
|
|
516
|
+
after: 'cursor-3',
|
|
517
|
+
}))
|
|
518
|
+
.mockResolvedValueOnce(mockResponse([createSearchResult(2)], {
|
|
519
|
+
hasMore: true,
|
|
520
|
+
total: 2,
|
|
521
|
+
after: 'cursor-3',
|
|
522
|
+
}));
|
|
523
|
+
const { result } = renderHook(() => useCrmSearch({ objectType: '0-1', pageSize: 10 }));
|
|
524
|
+
await waitFor(() => expect(result.current.pagination.currentPage).toBe(1));
|
|
525
|
+
result.current.pagination.nextPage();
|
|
526
|
+
await waitFor(() => expect(result.current.pagination.currentPage).toBe(2));
|
|
527
|
+
await result.current.refetch();
|
|
528
|
+
await waitFor(() => {
|
|
529
|
+
expect(result.current.pagination.currentPage).toBe(2);
|
|
530
|
+
expect(result.current.pagination.hasNextPage).toBe(true);
|
|
531
|
+
expect(result.current.pagination.hasPreviousPage).toBe(true);
|
|
532
|
+
});
|
|
533
|
+
expect(mockFetchCrmSearch).toHaveBeenLastCalledWith(expect.objectContaining({ after: 'cursor-2' }), expect.any(Object));
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
describe('lifecycle management', () => {
|
|
537
|
+
it('should call cleanup function on unmount', async () => {
|
|
538
|
+
const mockCleanup = vi.fn();
|
|
539
|
+
mockFetchCrmSearch.mockResolvedValue({
|
|
540
|
+
data: { results: [], total: 0, hasMore: false },
|
|
541
|
+
cleanup: mockCleanup,
|
|
542
|
+
});
|
|
543
|
+
const { unmount } = renderHook(() => useCrmSearch({ objectType: '0-1', pageSize: 10 }));
|
|
544
|
+
await waitFor(() => {
|
|
545
|
+
expect(mockFetchCrmSearch).toHaveBeenCalled();
|
|
546
|
+
});
|
|
547
|
+
unmount();
|
|
548
|
+
await waitFor(() => {
|
|
549
|
+
expect(mockCleanup).toHaveBeenCalled();
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
it('should handle stable reference optimization for config', async () => {
|
|
553
|
+
mockFetchCrmSearch.mockResolvedValue(mockResponse([]));
|
|
554
|
+
const config = {
|
|
555
|
+
objectType: '0-1',
|
|
556
|
+
properties: ['firstname'],
|
|
557
|
+
pageSize: 10,
|
|
558
|
+
};
|
|
559
|
+
const { rerender } = renderHook(({ configProp }) => useCrmSearch(configProp), { initialProps: { configProp: config } });
|
|
560
|
+
await waitFor(() => {
|
|
561
|
+
expect(mockFetchCrmSearch).toHaveBeenCalledTimes(1);
|
|
562
|
+
});
|
|
563
|
+
rerender({ configProp: { ...config } });
|
|
564
|
+
await waitFor(() => {
|
|
565
|
+
expect(mockFetchCrmSearch).toHaveBeenCalledTimes(1);
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
it('should refetch when query changes', async () => {
|
|
569
|
+
mockFetchCrmSearch.mockResolvedValue(mockResponse([]));
|
|
570
|
+
const { rerender } = renderHook(({ query }) => useCrmSearch({
|
|
571
|
+
objectType: '0-1',
|
|
572
|
+
properties: ['firstname'],
|
|
573
|
+
pageSize: 10,
|
|
574
|
+
query,
|
|
575
|
+
}), { initialProps: { query: 'john' } });
|
|
576
|
+
await waitFor(() => {
|
|
577
|
+
expect(mockFetchCrmSearch).toHaveBeenCalledTimes(1);
|
|
578
|
+
});
|
|
579
|
+
rerender({ query: 'jane' });
|
|
580
|
+
await waitFor(() => {
|
|
581
|
+
expect(mockFetchCrmSearch).toHaveBeenCalledTimes(2);
|
|
582
|
+
expect(mockFetchCrmSearch).toHaveBeenLastCalledWith(expect.objectContaining({ query: 'jane' }), expect.any(Object));
|
|
583
|
+
});
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
});
|