| ... | ... | @@ -1,7 +1,11 @@ |
| 1 | 1 | import { ProjectId, ThreadId } from "@t3tools/contracts"; |
| 2 | 2 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
| 3 | 3 | |
| 4 | | import { type ComposerImageAttachment, useComposerDraftStore } from "./composerDraftStore"; |
| 4 | import { |
| 5 | type ComposerImageAttachment, |
| 6 | createDebouncedStorage, |
| 7 | useComposerDraftStore, |
| 8 | } from "./composerDraftStore"; |
| 5 | 9 | |
| 6 | 10 | function makeImage(input: { |
| 7 | 11 | id: string; |
| ... | ... | @@ -451,3 +455,125 @@ describe("composerDraftStore runtime and interaction settings", () => { |
| 451 | 455 | expect(useComposerDraftStore.getState().draftsByThreadId[threadId]).toBeUndefined(); |
| 452 | 456 | }); |
| 453 | 457 | }); |
| 458 | |
| 459 | // --------------------------------------------------------------------------- |
| 460 | // createDebouncedStorage |
| 461 | // --------------------------------------------------------------------------- |
| 462 | |
| 463 | function createMockStorage() { |
| 464 | const store = new Map<string, string>(); |
| 465 | return { |
| 466 | getItem: vi.fn((name: string) => store.get(name) ?? null), |
| 467 | setItem: vi.fn((name: string, value: string) => { |
| 468 | store.set(name, value); |
| 469 | }), |
| 470 | removeItem: vi.fn((name: string) => { |
| 471 | store.delete(name); |
| 472 | }), |
| 473 | }; |
| 474 | } |
| 475 | |
| 476 | describe("createDebouncedStorage", () => { |
| 477 | beforeEach(() => { |
| 478 | vi.useFakeTimers(); |
| 479 | }); |
| 480 | |
| 481 | afterEach(() => { |
| 482 | vi.useRealTimers(); |
| 483 | }); |
| 484 | |
| 485 | it("delegates getItem immediately", () => { |
| 486 | const base = createMockStorage(); |
| 487 | base.getItem.mockReturnValueOnce("value"); |
| 488 | const storage = createDebouncedStorage(base); |
| 489 | |
| 490 | expect(storage.getItem("key")).toBe("value"); |
| 491 | expect(base.getItem).toHaveBeenCalledWith("key"); |
| 492 | }); |
| 493 | |
| 494 | it("does not write to base storage until the debounce fires", () => { |
| 495 | const base = createMockStorage(); |
| 496 | const storage = createDebouncedStorage(base); |
| 497 | |
| 498 | storage.setItem("key", "v1"); |
| 499 | expect(base.setItem).not.toHaveBeenCalled(); |
| 500 | |
| 501 | vi.advanceTimersByTime(299); |
| 502 | expect(base.setItem).not.toHaveBeenCalled(); |
| 503 | |
| 504 | vi.advanceTimersByTime(1); |
| 505 | expect(base.setItem).toHaveBeenCalledWith("key", "v1"); |
| 506 | }); |
| 507 | |
| 508 | it("only writes the last value when setItem is called rapidly", () => { |
| 509 | const base = createMockStorage(); |
| 510 | const storage = createDebouncedStorage(base); |
| 511 | |
| 512 | storage.setItem("key", "v1"); |
| 513 | storage.setItem("key", "v2"); |
| 514 | storage.setItem("key", "v3"); |
| 515 | |
| 516 | vi.advanceTimersByTime(300); |
| 517 | expect(base.setItem).toHaveBeenCalledTimes(1); |
| 518 | expect(base.setItem).toHaveBeenCalledWith("key", "v3"); |
| 519 | }); |
| 520 | |
| 521 | it("removeItem cancels a pending setItem write", () => { |
| 522 | const base = createMockStorage(); |
| 523 | const storage = createDebouncedStorage(base); |
| 524 | |
| 525 | storage.setItem("key", "v1"); |
| 526 | storage.removeItem("key"); |
| 527 | |
| 528 | vi.advanceTimersByTime(300); |
| 529 | expect(base.setItem).not.toHaveBeenCalled(); |
| 530 | expect(base.removeItem).toHaveBeenCalledWith("key"); |
| 531 | }); |
| 532 | |
| 533 | it("flush writes the pending value immediately", () => { |
| 534 | const base = createMockStorage(); |
| 535 | const storage = createDebouncedStorage(base); |
| 536 | |
| 537 | storage.setItem("key", "v1"); |
| 538 | expect(base.setItem).not.toHaveBeenCalled(); |
| 539 | |
| 540 | storage.flush(); |
| 541 | expect(base.setItem).toHaveBeenCalledWith("key", "v1"); |
| 542 | |
| 543 | // Timer should be cancelled; no duplicate write. |
| 544 | vi.advanceTimersByTime(300); |
| 545 | expect(base.setItem).toHaveBeenCalledTimes(1); |
| 546 | }); |
| 547 | |
| 548 | it("flush is a no-op when nothing is pending", () => { |
| 549 | const base = createMockStorage(); |
| 550 | const storage = createDebouncedStorage(base); |
| 551 | |
| 552 | storage.flush(); |
| 553 | expect(base.setItem).not.toHaveBeenCalled(); |
| 554 | }); |
| 555 | |
| 556 | it("flush after removeItem is a no-op", () => { |
| 557 | const base = createMockStorage(); |
| 558 | const storage = createDebouncedStorage(base); |
| 559 | |
| 560 | storage.setItem("key", "v1"); |
| 561 | storage.removeItem("key"); |
| 562 | storage.flush(); |
| 563 | |
| 564 | expect(base.setItem).not.toHaveBeenCalled(); |
| 565 | }); |
| 566 | |
| 567 | it("setItem works normally after removeItem cancels a pending write", () => { |
| 568 | const base = createMockStorage(); |
| 569 | const storage = createDebouncedStorage(base); |
| 570 | |
| 571 | storage.setItem("key", "v1"); |
| 572 | storage.removeItem("key"); |
| 573 | storage.setItem("key", "v2"); |
| 574 | |
| 575 | vi.advanceTimersByTime(300); |
| 576 | expect(base.setItem).toHaveBeenCalledTimes(1); |
| 577 | expect(base.setItem).toHaveBeenCalledWith("key", "v2"); |
| 578 | }); |
| 579 | }); |