@contractspec/example.crm-pipeline 3.7.7 → 3.7.12

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 (57) hide show
  1. package/.turbo/turbo-build.log +45 -42
  2. package/CHANGELOG.md +72 -0
  3. package/README.md +2 -1
  4. package/dist/browser/docs/crm-pipeline.docblock.js +1 -1
  5. package/dist/browser/docs/index.js +1 -1
  6. package/dist/browser/handlers/crm.handlers.js +13 -2
  7. package/dist/browser/handlers/index.js +13 -2
  8. package/dist/browser/index.js +392 -159
  9. package/dist/browser/ui/CrmDashboard.js +366 -144
  10. package/dist/browser/ui/hooks/index.js +19 -8
  11. package/dist/browser/ui/hooks/useDealList.js +19 -8
  12. package/dist/browser/ui/index.js +391 -158
  13. package/dist/browser/ui/renderers/index.js +32 -10
  14. package/dist/browser/ui/renderers/pipeline.markdown.js +13 -2
  15. package/dist/browser/ui/renderers/pipeline.renderer.js +19 -8
  16. package/dist/browser/ui/tables/DealListTab.js +390 -0
  17. package/dist/docs/crm-pipeline.docblock.js +1 -1
  18. package/dist/docs/index.js +1 -1
  19. package/dist/handlers/crm.handlers.d.ts +2 -0
  20. package/dist/handlers/crm.handlers.js +13 -2
  21. package/dist/handlers/index.js +13 -2
  22. package/dist/index.js +392 -159
  23. package/dist/node/docs/crm-pipeline.docblock.js +1 -1
  24. package/dist/node/docs/index.js +1 -1
  25. package/dist/node/handlers/crm.handlers.js +13 -2
  26. package/dist/node/handlers/index.js +13 -2
  27. package/dist/node/index.js +392 -159
  28. package/dist/node/ui/CrmDashboard.js +366 -144
  29. package/dist/node/ui/hooks/index.js +19 -8
  30. package/dist/node/ui/hooks/useDealList.js +19 -8
  31. package/dist/node/ui/index.js +391 -158
  32. package/dist/node/ui/renderers/index.js +32 -10
  33. package/dist/node/ui/renderers/pipeline.markdown.js +13 -2
  34. package/dist/node/ui/renderers/pipeline.renderer.js +19 -8
  35. package/dist/node/ui/tables/DealListTab.js +390 -0
  36. package/dist/ui/CrmDashboard.js +366 -144
  37. package/dist/ui/hooks/index.js +19 -8
  38. package/dist/ui/hooks/useDealList.d.ts +8 -2
  39. package/dist/ui/hooks/useDealList.js +19 -8
  40. package/dist/ui/index.js +391 -158
  41. package/dist/ui/renderers/index.js +32 -10
  42. package/dist/ui/renderers/pipeline.markdown.d.ts +1 -1
  43. package/dist/ui/renderers/pipeline.markdown.js +13 -2
  44. package/dist/ui/renderers/pipeline.renderer.d.ts +1 -1
  45. package/dist/ui/renderers/pipeline.renderer.js +19 -8
  46. package/dist/ui/tables/DealListTab.d.ts +20 -0
  47. package/dist/ui/tables/DealListTab.js +391 -0
  48. package/dist/ui/tables/DealListTab.smoke.test.d.ts +1 -0
  49. package/package.json +29 -13
  50. package/src/docs/crm-pipeline.docblock.ts +1 -1
  51. package/src/handlers/crm.handlers.ts +18 -1
  52. package/src/ui/CrmDashboard.tsx +2 -71
  53. package/src/ui/hooks/useDealList.ts +36 -8
  54. package/src/ui/renderers/pipeline.markdown.ts +1 -1
  55. package/src/ui/renderers/pipeline.renderer.tsx +1 -1
  56. package/src/ui/tables/DealListTab.smoke.test.tsx +149 -0
  57. package/src/ui/tables/DealListTab.tsx +276 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contractspec/example.crm-pipeline",
3
- "version": "3.7.7",
3
+ "version": "3.7.12",
4
4
  "description": "CRM Pipeline - Contacts, Companies, Deals, Tasks",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",
@@ -312,6 +312,13 @@
312
312
  "bun": "./dist/ui/renderers/pipeline.renderer.js",
313
313
  "node": "./dist/node/ui/renderers/pipeline.renderer.js",
314
314
  "default": "./dist/ui/renderers/pipeline.renderer.js"
315
+ },
316
+ "./ui/tables/DealListTab": {
317
+ "types": "./dist/ui/tables/DealListTab.d.ts",
318
+ "browser": "./dist/browser/ui/tables/DealListTab.js",
319
+ "bun": "./dist/ui/tables/DealListTab.js",
320
+ "node": "./dist/node/ui/tables/DealListTab.js",
321
+ "default": "./dist/ui/tables/DealListTab.js"
315
322
  }
316
323
  },
317
324
  "scripts": {
@@ -331,24 +338,26 @@
331
338
  "typecheck": "tsc --noEmit"
332
339
  },
333
340
  "dependencies": {
334
- "@contractspec/lib.contracts-spec": "4.0.0",
335
- "@contractspec/lib.design-system": "3.8.0",
336
- "@contractspec/lib.example-shared-ui": "6.0.7",
337
- "@contractspec/lib.identity-rbac": "3.7.7",
338
- "@contractspec/lib.runtime-sandbox": "2.7.6",
339
- "@contractspec/lib.schema": "3.7.6",
340
- "@contractspec/lib.ui-kit-web": "3.8.0",
341
- "@contractspec/module.audit-trail": "3.7.7",
342
- "@contractspec/module.notifications": "3.7.7",
341
+ "@contractspec/lib.contracts-spec": "5.0.0",
342
+ "@contractspec/lib.design-system": "3.8.5",
343
+ "@contractspec/lib.example-shared-ui": "6.0.12",
344
+ "@contractspec/lib.identity-rbac": "3.7.12",
345
+ "@contractspec/lib.runtime-sandbox": "2.7.10",
346
+ "@contractspec/lib.schema": "3.7.10",
347
+ "@contractspec/lib.ui-kit-web": "3.9.4",
348
+ "@contractspec/module.audit-trail": "3.7.12",
349
+ "@contractspec/module.notifications": "3.7.12",
343
350
  "react": "19.2.0",
344
- "react-dom": "19.2.0"
351
+ "react-dom": "19.2.0",
352
+ "@contractspec/lib.presentation-runtime-core": "3.9.0"
345
353
  },
346
354
  "devDependencies": {
347
- "@contractspec/tool.typescript": "3.7.6",
355
+ "@contractspec/tool.typescript": "3.7.9",
348
356
  "typescript": "^5.9.3",
349
357
  "@types/react": "^19.2.14",
350
358
  "@types/react-dom": "^19.2.2",
351
- "@contractspec/tool.bun": "3.7.6"
359
+ "@contractspec/tool.bun": "3.7.9",
360
+ "happy-dom": "^20.8.4"
352
361
  },
353
362
  "publishConfig": {
354
363
  "exports": {
@@ -659,6 +668,13 @@
659
668
  "bun": "./dist/ui/renderers/pipeline.renderer.js",
660
669
  "node": "./dist/node/ui/renderers/pipeline.renderer.js",
661
670
  "default": "./dist/ui/renderers/pipeline.renderer.js"
671
+ },
672
+ "./ui/tables/DealListTab": {
673
+ "types": "./dist/ui/tables/DealListTab.d.ts",
674
+ "browser": "./dist/browser/ui/tables/DealListTab.js",
675
+ "bun": "./dist/ui/tables/DealListTab.js",
676
+ "node": "./dist/node/ui/tables/DealListTab.js",
677
+ "default": "./dist/ui/tables/DealListTab.js"
662
678
  }
663
679
  },
664
680
  "registry": "https://registry.npmjs.org/",
@@ -84,7 +84,7 @@ const crmPipelineDocBlocks: DocBlock[] = [
84
84
  - deal.created, stage.moved, task.completed, contact.updated.
85
85
 
86
86
  ## Presentations
87
- - Pipelines/kanban, deal detail, contact/company profiles, task lists.
87
+ - Pipelines/kanban, deal detail, contact/company profiles, task lists, and a server-mode shared table for the deal list.
88
88
 
89
89
  ## Notes
90
90
  - Stage definitions should be declarative; enforce via spec and regeneration.
@@ -76,6 +76,8 @@ export interface ListDealsInput {
76
76
  search?: string;
77
77
  limit?: number;
78
78
  offset?: number;
79
+ sortBy?: 'name' | 'value' | 'status' | 'expectedCloseDate' | 'updatedAt';
80
+ sortDirection?: 'asc' | 'desc';
79
81
  }
80
82
 
81
83
  export interface ListDealsOutput {
@@ -142,6 +144,17 @@ function rowToDeal(row: DealRow): Deal {
142
144
 
143
145
  // ============ Handler Factory ============
144
146
 
147
+ const DEAL_SORT_COLUMNS: Record<
148
+ NonNullable<ListDealsInput['sortBy']>,
149
+ string
150
+ > = {
151
+ name: 'name',
152
+ value: 'value',
153
+ status: 'status',
154
+ expectedCloseDate: 'expectedCloseDate',
155
+ updatedAt: 'updatedAt',
156
+ };
157
+
145
158
  export function createCrmHandlers(db: DatabasePort) {
146
159
  /**
147
160
  * List deals with filtering
@@ -156,6 +169,8 @@ export function createCrmHandlers(db: DatabasePort) {
156
169
  search,
157
170
  limit = 20,
158
171
  offset = 0,
172
+ sortBy = 'value',
173
+ sortDirection = 'desc',
159
174
  } = input;
160
175
 
161
176
  let whereClause = 'WHERE projectId = ?';
@@ -205,9 +220,11 @@ export function createCrmHandlers(db: DatabasePort) {
205
220
  const totalValue = (valueResult[0]?.total as number) ?? 0;
206
221
 
207
222
  // Get paginated deals
223
+ const orderByColumn = DEAL_SORT_COLUMNS[sortBy] ?? DEAL_SORT_COLUMNS.value;
224
+ const orderByDirection = sortDirection === 'asc' ? 'ASC' : 'DESC';
208
225
  const dealRows = (
209
226
  await db.query(
210
- `SELECT * FROM crm_deal ${whereClause} ORDER BY value DESC LIMIT ? OFFSET ?`,
227
+ `SELECT * FROM crm_deal ${whereClause} ORDER BY ${orderByColumn} ${orderByDirection} LIMIT ? OFFSET ?`,
211
228
  [...params, limit, offset]
212
229
  )
213
230
  ).rows as unknown as DealRow[];
@@ -31,6 +31,7 @@ import { type Deal, useDealList } from './hooks/useDealList';
31
31
  import { useDealMutations } from './hooks/useDealMutations';
32
32
  import { CreateDealModal } from './modals/CreateDealModal';
33
33
  import { DealActionsModal } from './modals/DealActionsModal';
34
+ import { DealListTab } from './tables/DealListTab';
34
35
 
35
36
  // type Tab = 'pipeline' | 'list' | 'metrics';
36
37
 
@@ -155,7 +156,7 @@ export function CrmDashboard() {
155
156
  </TabsContent>
156
157
 
157
158
  <TabsContent value="list" className="min-h-[400px]">
158
- <DealListTab data={data} onDealClick={handleDealClick} />
159
+ <DealListTab onDealClick={handleDealClick} />
159
160
  </TabsContent>
160
161
 
161
162
  <TabsContent value="metrics" className="min-h-[400px]">
@@ -199,76 +200,6 @@ export function CrmDashboard() {
199
200
  );
200
201
  }
201
202
 
202
- interface DealListTabProps {
203
- data: ReturnType<typeof useDealList>['data'];
204
- onDealClick?: (dealId: string) => void;
205
- }
206
-
207
- function DealListTab({ data, onDealClick }: DealListTabProps) {
208
- if (!data?.deals.length) {
209
- return (
210
- <div className="flex h-64 items-center justify-center text-muted-foreground">
211
- No deals found
212
- </div>
213
- );
214
- }
215
-
216
- return (
217
- <div className="rounded-lg border border-border">
218
- <table className="w-full">
219
- <thead className="border-border border-b bg-muted/30">
220
- <tr>
221
- <th className="px-4 py-3 text-left font-medium text-sm">Deal</th>
222
- <th className="px-4 py-3 text-left font-medium text-sm">Value</th>
223
- <th className="px-4 py-3 text-left font-medium text-sm">Status</th>
224
- <th className="px-4 py-3 text-left font-medium text-sm">
225
- Expected Close
226
- </th>
227
- <th className="px-4 py-3 text-left font-medium text-sm">Actions</th>
228
- </tr>
229
- </thead>
230
- <tbody className="divide-y divide-border">
231
- {data.deals.map((deal: Deal) => (
232
- <tr key={deal.id} className="hover:bg-muted/50">
233
- <td className="px-4 py-3">
234
- <div className="font-medium">{deal.name}</div>
235
- </td>
236
- <td className="px-4 py-3 font-mono">
237
- {formatCurrency(deal.value, deal.currency)}
238
- </td>
239
- <td className="px-4 py-3">
240
- <span
241
- className={`inline-flex rounded-full px-2 py-0.5 font-medium text-xs ${
242
- deal.status === 'WON'
243
- ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
244
- : deal.status === 'LOST'
245
- ? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
246
- : 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
247
- }`}
248
- >
249
- {deal.status}
250
- </span>
251
- </td>
252
- <td className="px-4 py-3 text-muted-foreground">
253
- {deal.expectedCloseDate?.toLocaleDateString() ?? '-'}
254
- </td>
255
- <td className="px-4 py-3">
256
- <Button
257
- variant="ghost"
258
- size="sm"
259
- onPress={() => onDealClick?.(deal.id)}
260
- >
261
- Actions
262
- </Button>
263
- </td>
264
- </tr>
265
- ))}
266
- </tbody>
267
- </table>
268
- </div>
269
- );
270
- }
271
-
272
203
  function MetricsTab({
273
204
  stats,
274
205
  }: {
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useTemplateRuntime } from '@contractspec/lib.example-shared-ui';
4
+ import type { ContractTableSort } from '@contractspec/lib.presentation-runtime-core';
4
5
  /**
5
6
  * Hook for fetching and managing deal list data
6
7
  *
@@ -24,6 +25,9 @@ export interface UseDealListOptions {
24
25
  status?: 'OPEN' | 'WON' | 'LOST' | 'all';
25
26
  search?: string;
26
27
  limit?: number;
28
+ pageIndex?: number;
29
+ pageSize?: number;
30
+ sorting?: ContractTableSort[];
27
31
  }
28
32
 
29
33
  export function useDealList(options: UseDealListOptions = {}) {
@@ -35,9 +39,14 @@ export function useDealList(options: UseDealListOptions = {}) {
35
39
  const [stages, setStages] = useState<Stage[]>([]);
36
40
  const [loading, setLoading] = useState(true);
37
41
  const [error, setError] = useState<Error | null>(null);
38
- const [page, setPage] = useState(1);
42
+ const [internalPage, setInternalPage] = useState(0);
39
43
 
40
44
  const pipelineId = options.pipelineId ?? 'pipeline-1';
45
+ const pageIndex = options.pageIndex ?? internalPage;
46
+ const pageSize = options.pageSize ?? options.limit ?? 50;
47
+ const [sort] = options.sorting ?? [];
48
+ const sortBy = sort?.id;
49
+ const sortDirection = sort ? (sort.desc ? 'desc' : 'asc') : undefined;
41
50
 
42
51
  const fetchData = useCallback(async () => {
43
52
  setLoading(true);
@@ -51,8 +60,17 @@ export function useDealList(options: UseDealListOptions = {}) {
51
60
  stageId: options.stageId,
52
61
  status: options.status === 'all' ? undefined : options.status,
53
62
  search: options.search,
54
- limit: options.limit ?? 50,
55
- offset: (page - 1) * (options.limit ?? 50),
63
+ limit: pageSize,
64
+ offset: pageIndex * pageSize,
65
+ sortBy:
66
+ sortBy === 'name' ||
67
+ sortBy === 'value' ||
68
+ sortBy === 'status' ||
69
+ sortBy === 'expectedCloseDate' ||
70
+ sortBy === 'updatedAt'
71
+ ? sortBy
72
+ : undefined,
73
+ sortDirection,
56
74
  }),
57
75
  crm.getDealsByStage({ projectId, pipelineId }),
58
76
  crm.getPipelineStages({ pipelineId }),
@@ -72,8 +90,10 @@ export function useDealList(options: UseDealListOptions = {}) {
72
90
  options.stageId,
73
91
  options.status,
74
92
  options.search,
75
- options.limit,
76
- page,
93
+ pageIndex,
94
+ pageSize,
95
+ sortBy,
96
+ sortDirection,
77
97
  ]);
78
98
 
79
99
  useEffect(() => {
@@ -105,9 +125,17 @@ export function useDealList(options: UseDealListOptions = {}) {
105
125
  loading,
106
126
  error,
107
127
  stats,
108
- page,
128
+ page: pageIndex + 1,
129
+ pageIndex,
130
+ pageSize,
109
131
  refetch: fetchData,
110
- nextPage: () => setPage((p) => p + 1),
111
- prevPage: () => page > 1 && setPage((p) => p - 1),
132
+ nextPage:
133
+ options.pageIndex === undefined
134
+ ? () => setInternalPage((page) => page + 1)
135
+ : undefined,
136
+ prevPage:
137
+ options.pageIndex === undefined
138
+ ? () => pageIndex > 0 && setInternalPage((page) => page - 1)
139
+ : undefined,
112
140
  };
113
141
  }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Imports handlers from the hooks module to ensure correct build order.
5
5
  */
6
- import type { PresentationRenderer } from '@contractspec/lib.contracts-spec/presentations/transform-engine';
6
+ import type { PresentationRenderer } from '@contractspec/lib.presentation-runtime-core/transform-engine';
7
7
  import {
8
8
  mockGetPipelineStagesHandler,
9
9
  mockListDealsHandler,
@@ -5,7 +5,7 @@
5
5
  * Data is fetched via the CrmPipelineBoard component's internal hooks.
6
6
  */
7
7
 
8
- import type { PresentationRenderer } from '@contractspec/lib.contracts-spec/presentations/transform-engine';
8
+ import type { PresentationRenderer } from '@contractspec/lib.presentation-runtime-core/transform-engine';
9
9
  import * as React from 'react';
10
10
  import { CrmPipelineBoard } from '../CrmPipelineBoard';
11
11
  import { useDealList } from '../hooks/useDealList';
@@ -0,0 +1,149 @@
1
+ import { afterEach, beforeAll, describe, expect, test } from 'bun:test';
2
+ import Window from 'happy-dom/lib/window/Window.js';
3
+ import * as React from 'react';
4
+ import { act } from 'react';
5
+ import { createRoot, type Root } from 'react-dom/client';
6
+ import { MOCK_DEALS } from '../../handlers/mock-data';
7
+ import type { Deal } from '../hooks/useDealList';
8
+ import { DealListDataTable } from './DealListTab';
9
+
10
+ const TEST_DEALS: Deal[] = MOCK_DEALS.map((deal) => ({
11
+ ...deal,
12
+ projectId: 'project-1',
13
+ }));
14
+
15
+ beforeAll(() => {
16
+ const windowInstance = new Window({
17
+ url: 'https://sandbox.contractspec.local/sandbox?template=crm-pipeline',
18
+ });
19
+ Object.defineProperty(windowInstance, 'SyntaxError', {
20
+ value: SyntaxError,
21
+ configurable: true,
22
+ });
23
+ Object.assign(globalThis, {
24
+ window: windowInstance,
25
+ document: windowInstance.document,
26
+ navigator: windowInstance.navigator,
27
+ HTMLElement: windowInstance.HTMLElement,
28
+ HTMLButtonElement: windowInstance.HTMLButtonElement,
29
+ Node: windowInstance.Node,
30
+ Event: windowInstance.Event,
31
+ MouseEvent: windowInstance.MouseEvent,
32
+ MutationObserver: windowInstance.MutationObserver,
33
+ getComputedStyle: windowInstance.getComputedStyle.bind(windowInstance),
34
+ requestAnimationFrame: (callback: FrameRequestCallback) =>
35
+ setTimeout(() => callback(Date.now()), 0),
36
+ cancelAnimationFrame: (id: number) => clearTimeout(id),
37
+ IS_REACT_ACT_ENVIRONMENT: true,
38
+ });
39
+ });
40
+
41
+ afterEach(() => {
42
+ document.body.innerHTML = '';
43
+ });
44
+
45
+ function sortDeals(
46
+ pageIndex: number,
47
+ pageSize: number,
48
+ sorting: { id: string; desc: boolean }[]
49
+ ) {
50
+ const [sort] = sorting;
51
+ const sorted = [...TEST_DEALS].sort((left, right) => {
52
+ const leftValue =
53
+ sort?.id === 'deal'
54
+ ? left.name
55
+ : sort?.id === 'status'
56
+ ? left.status
57
+ : sort?.id === 'expectedCloseDate'
58
+ ? (left.expectedCloseDate?.toISOString() ?? '')
59
+ : sort?.id === 'updatedAt'
60
+ ? left.updatedAt.toISOString()
61
+ : left.value;
62
+ const rightValue =
63
+ sort?.id === 'deal'
64
+ ? right.name
65
+ : sort?.id === 'status'
66
+ ? right.status
67
+ : sort?.id === 'expectedCloseDate'
68
+ ? (right.expectedCloseDate?.toISOString() ?? '')
69
+ : sort?.id === 'updatedAt'
70
+ ? right.updatedAt.toISOString()
71
+ : right.value;
72
+ if (leftValue === rightValue) return 0;
73
+ const comparison = leftValue > rightValue ? 1 : -1;
74
+ return sort?.desc ? comparison * -1 : comparison;
75
+ });
76
+ return sorted.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize);
77
+ }
78
+
79
+ function Harness() {
80
+ const [sorting, setSorting] = React.useState([{ id: 'value', desc: true }]);
81
+ const [pagination, setPagination] = React.useState({
82
+ pageIndex: 0,
83
+ pageSize: 3,
84
+ });
85
+ return (
86
+ <DealListDataTable
87
+ deals={sortDeals(pagination.pageIndex, pagination.pageSize, sorting)}
88
+ totalItems={TEST_DEALS.length}
89
+ pageIndex={pagination.pageIndex}
90
+ pageSize={pagination.pageSize}
91
+ sorting={sorting}
92
+ onSortingChange={setSorting}
93
+ onPaginationChange={setPagination}
94
+ />
95
+ );
96
+ }
97
+
98
+ async function renderTable() {
99
+ const container = document.createElement('div');
100
+ document.body.append(container);
101
+ const root: Root = createRoot(container);
102
+
103
+ await act(async () => {
104
+ root.render(<Harness />);
105
+ });
106
+
107
+ return { container, root };
108
+ }
109
+
110
+ async function click(element: Element | null | undefined) {
111
+ if (!element) {
112
+ throw new Error('Expected clickable element.');
113
+ }
114
+ await act(async () => {
115
+ if ('click' in element && typeof element.click === 'function') {
116
+ element.click();
117
+ return;
118
+ }
119
+ element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
120
+ });
121
+ }
122
+
123
+ describe('DealListDataTable', () => {
124
+ test('renders the shared table and supports selection plus expansion', async () => {
125
+ const { container, root } = await renderTable();
126
+
127
+ expect(container.textContent).toContain('All Deals');
128
+ expect(container.textContent).toContain('6 total deals');
129
+
130
+ await click(container.querySelector('[aria-label="Select row deal-5"]'));
131
+ expect(container.textContent).toContain('Selected 1');
132
+
133
+ await click(container.querySelector('[aria-label="Expand row deal-5"]'));
134
+ expect(container.textContent).toContain('user-1');
135
+
136
+ await click(
137
+ [...container.getElementsByTagName('button')].find(
138
+ (button) => button.textContent?.trim() === '2'
139
+ )
140
+ );
141
+ expect(container.textContent).toContain(
142
+ 'Affichage de 4 à 6 sur 6 résultats'
143
+ );
144
+
145
+ await act(async () => {
146
+ root.unmount();
147
+ });
148
+ });
149
+ });