theo-agent-dashboard

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

useDraft.test.ts (7169B)


      1 import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
      2 import { renderHook, act } from "@testing-library/react";
      3 import { useDraft, clearDraft, migrateDraft, DRAFT_KEY_PREFIX } from "./useDraft";
      4 
      5 beforeEach(() => {
      6   localStorage.clear();
      7   vi.useFakeTimers({ shouldAdvanceTime: true });
      8 });
      9 
     10 afterEach(() => {
     11   vi.useRealTimers();
     12 });
     13 
     14 describe("useDraft", () => {
     15   it("returns empty string when no draft exists", () => {
     16     const { result } = renderHook(() => useDraft("session-1"));
     17     const [text] = result.current;
     18     expect(text).toBe("");
     19   });
     20 
     21   it("returns existing draft from localStorage", () => {
     22     localStorage.setItem(`${DRAFT_KEY_PREFIX}session-1`, "hello world");
     23     const { result } = renderHook(() => useDraft("session-1"));
     24     const [text] = result.current;
     25     expect(text).toBe("hello world");
     26   });
     27 
     28   it("uses 'new' key for null sessionId", () => {
     29     localStorage.setItem(`${DRAFT_KEY_PREFIX}new`, "draft for new session");
     30     const { result } = renderHook(() => useDraft(null));
     31     const [text] = result.current;
     32     expect(text).toBe("draft for new session");
     33   });
     34 
     35   it("saves draft to localStorage after debounce", () => {
     36     const { result } = renderHook(() => useDraft("session-1"));
     37     const [, setText] = result.current;
     38 
     39     act(() => {
     40       setText("new draft text");
     41     });
     42 
     43     // Not saved yet (debounced)
     44     expect(localStorage.getItem(`${DRAFT_KEY_PREFIX}session-1`)).toBeNull();
     45 
     46     // Advance past debounce (300ms)
     47     act(() => {
     48       vi.advanceTimersByTime(350);
     49     });
     50 
     51     expect(localStorage.getItem(`${DRAFT_KEY_PREFIX}session-1`)).toBe("new draft text");
     52   });
     53 
     54   it("debounces rapid saves — only last value persists", () => {
     55     const { result } = renderHook(() => useDraft("session-1"));
     56     const [, setText] = result.current;
     57 
     58     act(() => {
     59       setText("a");
     60       vi.advanceTimersByTime(100);
     61       setText("ab");
     62       vi.advanceTimersByTime(100);
     63       setText("abc");
     64     });
     65 
     66     act(() => {
     67       vi.advanceTimersByTime(350);
     68     });
     69 
     70     expect(localStorage.getItem(`${DRAFT_KEY_PREFIX}session-1`)).toBe("abc");
     71   });
     72 
     73   it("restores draft when sessionId changes", () => {
     74     localStorage.setItem(`${DRAFT_KEY_PREFIX}s1`, "draft one");
     75     localStorage.setItem(`${DRAFT_KEY_PREFIX}s2`, "draft two");
     76 
     77     const { result, rerender } = renderHook(
     78       ({ sessionId }) => useDraft(sessionId),
     79       { initialProps: { sessionId: "s1" as string | null } },
     80     );
     81 
     82     expect(result.current[0]).toBe("draft one");
     83 
     84     rerender({ sessionId: "s2" });
     85     expect(result.current[0]).toBe("draft two");
     86   });
     87 
     88   it("clears in-memory text when switching to session with no draft", () => {
     89     const { result, rerender } = renderHook(
     90       ({ sessionId }) => useDraft(sessionId),
     91       { initialProps: { sessionId: "s1" as string | null } },
     92     );
     93 
     94     act(() => {
     95       result.current[1]("some text");
     96     });
     97 
     98     rerender({ sessionId: "s2" });
     99     expect(result.current[0]).toBe("");
    100   });
    101 });
    102 
    103 describe("clearDraft", () => {
    104   it("removes draft from localStorage for given session", () => {
    105     localStorage.setItem(`${DRAFT_KEY_PREFIX}session-1`, "to be cleared");
    106     clearDraft("session-1");
    107     expect(localStorage.getItem(`${DRAFT_KEY_PREFIX}session-1`)).toBeNull();
    108   });
    109 
    110   it("removes 'new' draft when sessionId is null", () => {
    111     localStorage.setItem(`${DRAFT_KEY_PREFIX}new`, "new draft");
    112     clearDraft(null);
    113     expect(localStorage.getItem(`${DRAFT_KEY_PREFIX}new`)).toBeNull();
    114   });
    115 
    116   it("is a no-op when no draft exists", () => {
    117     clearDraft("nonexistent");
    118     // no error thrown
    119   });
    120 });
    121 
    122 describe("markSent", () => {
    123   it("does not restore draft after markSent when sessionId changes", () => {
    124     const { result, rerender } = renderHook(
    125       ({ sessionId }) => useDraft(sessionId),
    126       { initialProps: { sessionId: null as string | null } },
    127     );
    128 
    129     // User types a message in the "new" (null) session
    130     act(() => {
    131       result.current[1]("hello world");
    132     });
    133 
    134     // User hits send — markSent + clear text (simulates Composer's handleSend)
    135     act(() => {
    136       result.current[2](); // markSent
    137       result.current[1](""); // setText("")
    138     });
    139 
    140     expect(result.current[0]).toBe("");
    141 
    142     // Session creation triggers sessionId change (null → "new-session-id")
    143     // migrateDraft copies text to the new key, but markSent should suppress restore
    144     localStorage.setItem(`${DRAFT_KEY_PREFIX}new-session-id`, "hello world");
    145     rerender({ sessionId: "new-session-id" });
    146 
    147     // Text should stay empty — the draft should NOT be restored
    148     expect(result.current[0]).toBe("");
    149   });
    150 
    151   it("clears migrated draft from localStorage after markSent", () => {
    152     localStorage.setItem(`${DRAFT_KEY_PREFIX}new`, "old draft");
    153 
    154     const { result, rerender } = renderHook(
    155       ({ sessionId }) => useDraft(sessionId),
    156       { initialProps: { sessionId: null as string | null } },
    157     );
    158 
    159     act(() => {
    160       result.current[2](); // markSent
    161       result.current[1](""); // setText
    162     });
    163 
    164     // Simulate migrateDraft (ChatView does this before sessionId changes)
    165     localStorage.setItem(`${DRAFT_KEY_PREFIX}real-session`, "old draft");
    166     rerender({ sessionId: "real-session" });
    167 
    168     // The migrated draft should have been cleared from localStorage
    169     expect(localStorage.getItem(`${DRAFT_KEY_PREFIX}real-session`)).toBeNull();
    170   });
    171 
    172   it("allows normal draft restore on subsequent sessionId changes", () => {
    173     const { result, rerender } = renderHook(
    174       ({ sessionId }) => useDraft(sessionId),
    175       { initialProps: { sessionId: null as string | null } },
    176     );
    177 
    178     // First send — markSent suppresses restore
    179     act(() => {
    180       result.current[1]("msg 1");
    181       result.current[2](); // markSent
    182       result.current[1]("");
    183     });
    184 
    185     localStorage.setItem(`${DRAFT_KEY_PREFIX}s1`, "msg 1");
    186     rerender({ sessionId: "s1" });
    187     expect(result.current[0]).toBe(""); // suppressed
    188 
    189     // Set a draft for s2 and switch — should restore normally
    190     localStorage.setItem(`${DRAFT_KEY_PREFIX}s2`, "draft for s2");
    191     rerender({ sessionId: "s2" });
    192     expect(result.current[0]).toBe("draft for s2"); // normal restore
    193   });
    194 });
    195 
    196 describe("migrateDraft", () => {
    197   it("copies draft from 'new' to session ID and removes 'new'", () => {
    198     localStorage.setItem(`${DRAFT_KEY_PREFIX}new`, "my draft");
    199     migrateDraft(null, "created-session-id");
    200 
    201     expect(localStorage.getItem(`${DRAFT_KEY_PREFIX}new`)).toBeNull();
    202     expect(localStorage.getItem(`${DRAFT_KEY_PREFIX}created-session-id`)).toBe("my draft");
    203   });
    204 
    205   it("does nothing if source draft doesn't exist", () => {
    206     migrateDraft(null, "session-1");
    207     expect(localStorage.getItem(`${DRAFT_KEY_PREFIX}session-1`)).toBeNull();
    208   });
    209 
    210   it("overwrites target if it already exists", () => {
    211     localStorage.setItem(`${DRAFT_KEY_PREFIX}new`, "new draft");
    212     localStorage.setItem(`${DRAFT_KEY_PREFIX}session-1`, "old draft");
    213     migrateDraft(null, "session-1");
    214 
    215     expect(localStorage.getItem(`${DRAFT_KEY_PREFIX}new`)).toBeNull();
    216     expect(localStorage.getItem(`${DRAFT_KEY_PREFIX}session-1`)).toBe("new draft");
    217   });
    218 });