Skip to content
Source
Sign inSign up

discardComposerDraft.test.ts

121 lines4.8 KB

1import { EnvironmentId, ProjectId, ThreadId } from "@t3tools/contracts";
2import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment";
3import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
4​
5import {
6 type ComposerImageAttachment,
7 DraftId,
8 useComposerDraftStore,
9} from "../composerDraftStore";
10import { showThreadUndoNotice } from "../hooks/showThreadUndoNotice";
11import { discardComposerDraft } from "./discardComposerDraft";
12​
13vi.mock("./attachmentUploadQueue", () => ({ releaseDraftAttachments: vi.fn() }));
14vi.mock("../hooks/showThreadUndoNotice", () => ({ showThreadUndoNotice: vi.fn() }));
15​
16const environmentId = EnvironmentId.make("environment-local");
17const projectRef = scopeProjectRef(environmentId, ProjectId.make("project-1"));
18const draftId = DraftId.make("draft-1");
19const threadRef = scopeThreadRef(environmentId, ThreadId.make("thread-1"));
20​
21function undoNotice() {
22 const options = vi.mocked(showThreadUndoNotice).mock.calls.at(-1)?.[0];
23 if (!options) throw new Error("Undo notice is missing");
24 return options;
26​
27function imagePreview(name = "image.png") {
28 const file = new File(["image bytes"], name, { type: "image/png" });
29 const previewUrl = URL.createObjectURL(file);
30 return {
31 image: {
32 type: "image",
33 id: name,
34 name,
35 mimeType: file.type,
36 sizeBytes: file.size,
37 file,
38 previewUrl,
39 } satisfies ComposerImageAttachment,
40 [Symbol.dispose]() {
41 URL.revokeObjectURL(previewUrl);
42 },
43 };
45​
46beforeEach(() => {
47 vi.clearAllMocks();
48 useComposerDraftStore.setState({
49 draftsByThreadKey: {},
50 draftThreadsByThreadKey: {},
51 logicalProjectDraftThreadKeyByLogicalProjectKey: {},
52 });
53});
54​
55describe("discardComposerDraft", () => {
56 it("restores a discarded new-thread draft with its project mapping and a usable preview", async () => {
57 using preview = imagePreview();
58 const store = useComposerDraftStore.getState();
59 store.setProjectDraftThreadId(projectRef, draftId);
60 store.setPrompt(draftId, "half-written prompt");
61 store.addImage(draftId, preview.image);
62 const before = useComposerDraftStore.getState();
63​
64 discardComposerDraft(draftId);
65 expect(useComposerDraftStore.getState().getDraftSession(draftId)).toBeNull();
66 await expect(fetch(preview.image.previewUrl)).rejects.toThrow();
67​
68 await undoNotice().undo();
69 const after = useComposerDraftStore.getState();
70 expect(after.getDraftSession(draftId)).toEqual(before.getDraftSession(draftId));
71 expect(after.getComposerDraft(draftId)?.prompt).toBe("half-written prompt");
72 expect(after.logicalProjectDraftThreadKeyByLogicalProjectKey).toEqual(
73 before.logicalProjectDraftThreadKeyByLogicalProjectKey,
74 );
75 const restoredUrl = after.getComposerDraft(draftId)!.images[0]!.previewUrl;
76 try {
77 expect(await (await fetch(restoredUrl)).text()).toBe("image bytes");
78 } finally {
79 URL.revokeObjectURL(restoredUrl);
80 }
81 });
82​
83 it("releases a discarded existing-thread preview when Undo is committed", async () => {
84 using preview = imagePreview();
85 useComposerDraftStore.getState().addImage(threadRef, preview.image);
86​
87 discardComposerDraft(threadRef);
88 expect(useComposerDraftStore.getState().getComposerDraft(threadRef)?.images ?? []).toEqual([]);
89 expect(await (await fetch(preview.image.previewUrl)).text()).toBe("image bytes");
90​
91 undoNotice().commit?.();
92 await expect(fetch(preview.image.previewUrl)).rejects.toThrow();
93 });
94​
95 it("keeps newer draft content and releases the discarded preview when Undo is refused", async () => {
96 using oldPreview = imagePreview("old.png");
97 using newPreview = imagePreview("new.png");
98 useComposerDraftStore.getState().addImage(threadRef, oldPreview.image);
99 discardComposerDraft(threadRef);
100 useComposerDraftStore.getState().setPrompt(threadRef, "new reply");
101 useComposerDraftStore.getState().addImage(threadRef, newPreview.image);
102​
103 const result = await undoNotice().undo();
104 expect(result._tag).toBe("Failure");
105 expect(useComposerDraftStore.getState().getComposerDraft(threadRef)?.prompt).toBe("new reply");
106 await expect(fetch(oldPreview.image.previewUrl)).rejects.toThrow();
107 expect(await (await fetch(newPreview.image.previewUrl)).text()).toBe("image bytes");
108 });
109​
110 it("keeps an existing-thread preview usable after successful Undo", async () => {
111 using preview = imagePreview();
112 useComposerDraftStore.getState().addImage(threadRef, preview.image);
113 discardComposerDraft(threadRef);
114​
115 const result = await undoNotice().undo();
116 expect(result._tag).toBe("Success");
117 const restored = useComposerDraftStore.getState().getComposerDraft(threadRef)!.images[0]!;
118 expect(restored.previewUrl).toBe(preview.image.previewUrl);
119 expect(await (await fetch(restored.previewUrl)).text()).toBe("image bytes");
120 });
121});