1import { describe, expect, it, vi } from "vite-plus/test";
2
3const mocks = vi.hoisted(() => ({
4 files: new Set<string>(),
5 write: vi.fn(async (_content: string, _options: unknown) => undefined),
6}));
7
8vi.mock("expo-file-system", () => ({
9 Paths: { cache: "file:///cache" },
10 Directory: class {
11 constructor(
12 readonly parent: string,
13 readonly name: string,
14 ) {}
15 create() {}
16 },
17 File: class {
18 uri: string;
19 constructor(directory: { parent: string; name: string }, name: string) {
20 this.uri = `${directory.parent}/${directory.name}/${name}`;
21 }
22 get exists() {
23 return mocks.files.has(this.uri);
24 }
25 create() {
26 mocks.files.add(this.uri);
27 }
28 delete() {
29 mocks.files.delete(this.uri);
30 }
31 write(content: string, options: unknown) {
32 return mocks.write(content, options);
33 }
34 async move(destination: { uri: string }) {
35 if (!mocks.files.delete(this.uri)) throw new Error("source missing");
36 mocks.files.add(destination.uri);
37 this.uri = destination.uri;
38 }
39 },
40}));
41
42import type { EnvironmentId } from "@t3tools/contracts";
43import { AsyncResult, AtomRegistry } from "effect/reactivity";
44
45import type { DraftComposerImageAttachment } from "./composerImages";
46import { MaterializedPreviewKey, materializedPreviewAtom } from "./composerImagePreview";
47
48describe("materializedPreviewAtom", () => {
49 it("keeps the materialized preview when the upload reference lands", async () => {
50 const registry = AtomRegistry.make();
51 const attachment: DraftComposerImageAttachment = {
52 id: "image-1",
53 type: "image",
54 name: "photo.png",
55 mimeType: "image/png",
56 sizeBytes: 3,
57 previewUri: "data:image/png;base64,AAAA",
58 };
59 const atom = materializedPreviewAtom(new MaterializedPreviewKey(attachment));
60 const unmount = registry.mount(atom);
61 await vi.waitFor(() => expect(AsyncResult.isSuccess(registry.get(atom))).toBe(true));
62
63 const uploaded = materializedPreviewAtom(
64 new MaterializedPreviewKey({
65 ...attachment,
66 uploadedAttachmentId: "uploaded-1",
67 uploadEnvironmentId: "env-1" as EnvironmentId,
68 }),
69 );
70
71 expect(uploaded).toBe(atom);
72 const result = registry.get(uploaded);
73 expect(AsyncResult.isSuccess(result) && result.value).toBe(
74 "file:///cache/t3-composer-previews/image-1.png",
75 );
76 expect(mocks.write).toHaveBeenCalledTimes(1);
77 unmount();
78 });
79
80 it("writes the preview again after a failed write instead of reusing the partial file", async () => {
81 const attachment: DraftComposerImageAttachment = {
82 id: "image-failed",
83 type: "image",
84 name: "photo.png",
85 mimeType: "image/png",
86 sizeBytes: 3,
87 previewUri: "data:image/png;base64,AAAA",
88 };
89 const materialize = async () => {
90 const registry = AtomRegistry.make();
91 const atom = materializedPreviewAtom(new MaterializedPreviewKey(attachment));
92 const unmount = registry.mount(atom);
93 await vi.waitFor(() => expect(AsyncResult.isSuccess(registry.get(atom))).toBe(true));
94 const result = registry.get(atom);
95 unmount();
96 return AsyncResult.isSuccess(result) ? result.value : undefined;
97 };
98 using _warn = vi.spyOn(console, "warn").mockImplementation(() => {});
99 mocks.write.mockClear();
100 mocks.write.mockRejectedValueOnce(new Error("disk full"));
101
102 expect(await materialize()).toBeNull();
103 expect(await materialize()).toBe("file:///cache/t3-composer-previews/image-failed.png");
104 expect(mocks.write).toHaveBeenCalledTimes(2);
105 });
106
107 it("shares one write when the atom is rebuilt while its preview is still being written", async () => {
108 const attachment: DraftComposerImageAttachment = {
109 id: "image-rebuilt",
110 type: "image",
111 name: "pasted.jpeg",
112 mimeType: "image/jpeg",
113 sizeBytes: 3,
114 previewUri: "data:image/jpeg;base64,AAAA",
115 };
116 const read = () => {
117 const registry = AtomRegistry.make();
118 const atom = materializedPreviewAtom(new MaterializedPreviewKey(attachment));
119 const unmount = registry.mount(atom);
120 return vi
121 .waitFor(() => {
122 const result = registry.get(atom);
123 expect(AsyncResult.isSuccess(result)).toBe(true);
124 return AsyncResult.isSuccess(result) ? result.value : undefined;
125 })
126 .finally(unmount);
127 };
128
129 expect(await Promise.all([read(), read()])).toEqual([
130 "file:///cache/t3-composer-previews/image-rebuilt.jpg",
131 "file:///cache/t3-composer-previews/image-rebuilt.jpg",
132 ]);
133 });
134});