|
1 | 1 | import { renderHook } from "@testing-library/react"; |
2 | 2 | import { describe, it, expect, vi, beforeEach } from "vitest"; |
3 | | -import type { CollectionControl } from "@/types/collection"; |
| 3 | +import type { CollectionControl, TableMetadataMap } from "@/types/collection"; |
4 | 4 | import { |
5 | 5 | useUrlCollectionState, |
| 6 | + withURLState, |
6 | 7 | encodeFilterValue, |
7 | 8 | decodeFilterValue, |
8 | 9 | } from "./use-url-collection-state"; |
@@ -32,6 +33,32 @@ function resolveSetParamsCall(callIndex: number): URLSearchParams { |
32 | 33 | : new URLSearchParams(updaterOrValue); |
33 | 34 | } |
34 | 35 |
|
| 36 | +const tableMetadata = { |
| 37 | + task: { |
| 38 | + name: "task", |
| 39 | + pluralForm: "tasks", |
| 40 | + fields: [ |
| 41 | + { name: "status", type: "enum", required: true, enumValues: ["active", "pending", "closed"] }, |
| 42 | + { name: "createdAt", type: "datetime", required: true }, |
| 43 | + { name: "title", type: "string", required: true }, |
| 44 | + ], |
| 45 | + }, |
| 46 | +} as const satisfies TableMetadataMap; |
| 47 | + |
| 48 | +function resolveSearchParamsBindingCall( |
| 49 | + setSearchParams: ReturnType<typeof vi.fn>, |
| 50 | + prev = new URLSearchParams(), |
| 51 | +): URLSearchParams { |
| 52 | + const [updaterOrValue] = setSearchParams.mock.calls[0]; |
| 53 | + if (typeof updaterOrValue === "function") { |
| 54 | + const result = updaterOrValue(prev); |
| 55 | + return result instanceof URLSearchParams ? result : new URLSearchParams(result); |
| 56 | + } |
| 57 | + return updaterOrValue instanceof URLSearchParams |
| 58 | + ? updaterOrValue |
| 59 | + : new URLSearchParams(updaterOrValue); |
| 60 | +} |
| 61 | + |
35 | 62 | function makeControl(overrides?: Partial<CollectionControl>): CollectionControl { |
36 | 63 | return { |
37 | 64 | filters: [], |
@@ -255,6 +282,81 @@ describe("useUrlCollectionState", () => { |
255 | 282 | }); |
256 | 283 | }); |
257 | 284 |
|
| 285 | +describe("withURLState", () => { |
| 286 | + it("parses URL state and keeps params defaults intact", () => { |
| 287 | + const setSearchParams = vi.fn(); |
| 288 | + const options = withURLState( |
| 289 | + { |
| 290 | + tableMetadata: tableMetadata.task, |
| 291 | + params: { |
| 292 | + initialSort: [{ field: "createdAt", direction: "Desc" }], |
| 293 | + pageSize: 20, |
| 294 | + }, |
| 295 | + }, |
| 296 | + [new URLSearchParams("p=50&f.status:eq=active"), setSearchParams], |
| 297 | + ); |
| 298 | + |
| 299 | + expect(options.initialState).toEqual({ |
| 300 | + pageSize: 50, |
| 301 | + filters: [{ field: "status", operator: "eq", value: "active" }], |
| 302 | + }); |
| 303 | + expect(options.params).toEqual({ |
| 304 | + initialSort: [{ field: "createdAt", direction: "Desc" }], |
| 305 | + pageSize: 20, |
| 306 | + }); |
| 307 | + }); |
| 308 | + |
| 309 | + it("filters out URL fields/operators not allowed by tableMetadata", () => { |
| 310 | + const options = withURLState({ tableMetadata: tableMetadata.task }, [ |
| 311 | + new URLSearchParams( |
| 312 | + "s=amount:asc&f.amount:eq=10&f.createdAt:contains=2026&f.status:eq=active", |
| 313 | + ), |
| 314 | + vi.fn(), |
| 315 | + ]); |
| 316 | + |
| 317 | + expect(options.initialState).toEqual({ |
| 318 | + filters: [{ field: "status", operator: "eq", value: "active" }], |
| 319 | + }); |
| 320 | + }); |
| 321 | + |
| 322 | + it("merges existing initialState and composes saver", () => { |
| 323 | + const setSearchParams = vi.fn(); |
| 324 | + const baseSaver = { save: vi.fn() }; |
| 325 | + const options = withURLState( |
| 326 | + { |
| 327 | + tableMetadata: tableMetadata.task, |
| 328 | + initialState: { |
| 329 | + sortStates: [{ field: "createdAt", direction: "Desc" }], |
| 330 | + }, |
| 331 | + saver: baseSaver, |
| 332 | + }, |
| 333 | + [new URLSearchParams("p=25"), setSearchParams], |
| 334 | + ); |
| 335 | + |
| 336 | + expect(options.initialState).toEqual({ |
| 337 | + sortStates: [{ field: "createdAt", direction: "Desc" }], |
| 338 | + pageSize: 25, |
| 339 | + }); |
| 340 | + |
| 341 | + options.saver?.save({ |
| 342 | + filters: [{ field: "status", operator: "eq", value: "pending" }], |
| 343 | + sortStates: [{ field: "createdAt", direction: "Desc" }], |
| 344 | + pageSize: 30, |
| 345 | + }); |
| 346 | + |
| 347 | + expect(baseSaver.save).toHaveBeenCalledWith({ |
| 348 | + filters: [{ field: "status", operator: "eq", value: "pending" }], |
| 349 | + sortStates: [{ field: "createdAt", direction: "Desc" }], |
| 350 | + pageSize: 30, |
| 351 | + }); |
| 352 | + expect(setSearchParams).toHaveBeenCalledTimes(1); |
| 353 | + expect(setSearchParams.mock.calls[0][1]).toEqual({ replace: true }); |
| 354 | + expect(resolveSearchParamsBindingCall(setSearchParams).toString()).toBe( |
| 355 | + "p=30&s=createdAt%3Adesc&f.status%3Aeq=pending", |
| 356 | + ); |
| 357 | + }); |
| 358 | +}); |
| 359 | + |
258 | 360 | // --------------------------------------------------------------------------- |
259 | 361 | // Utility functions |
260 | 362 | // --------------------------------------------------------------------------- |
|
0 commit comments