jazz-tools 0.16.5 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/.svelte-kit/__package__/index.d.ts +1 -0
  2. package/.svelte-kit/__package__/index.d.ts.map +1 -1
  3. package/.svelte-kit/__package__/index.js +1 -0
  4. package/.svelte-kit/__package__/media/image.svelte +131 -0
  5. package/.svelte-kit/__package__/media/image.svelte.d.ts +10 -0
  6. package/.svelte-kit/__package__/media/image.svelte.d.ts.map +1 -0
  7. package/.svelte-kit/__package__/media/index.d.ts +2 -0
  8. package/.svelte-kit/__package__/media/index.d.ts.map +1 -0
  9. package/.svelte-kit/__package__/media/index.js +1 -0
  10. package/.svelte-kit/__package__/tests/media/image.svelte.test.d.ts +2 -0
  11. package/.svelte-kit/__package__/tests/media/image.svelte.test.d.ts.map +1 -0
  12. package/.svelte-kit/__package__/tests/media/image.svelte.test.js +430 -0
  13. package/.svelte-kit/__package__/tests/testUtils.d.ts +11 -0
  14. package/.svelte-kit/__package__/tests/testUtils.d.ts.map +1 -0
  15. package/.svelte-kit/__package__/tests/testUtils.js +17 -0
  16. package/.svelte-kit/__package__/tests/types.d.ts +3 -0
  17. package/.turbo/turbo-build.log +44 -48
  18. package/CHANGELOG.md +28 -0
  19. package/dist/{chunk-H3BIFFQG.js → chunk-2SH44VLX.js} +35 -40
  20. package/dist/chunk-2SH44VLX.js.map +1 -0
  21. package/dist/index.js +1 -3
  22. package/dist/index.js.map +1 -1
  23. package/dist/inspector/{custom-element-TUXKXSZU.js → custom-element-I7L56H6B.js} +3 -5
  24. package/dist/inspector/{custom-element-TUXKXSZU.js.map → custom-element-I7L56H6B.js.map} +1 -1
  25. package/dist/inspector/index.js +2 -4
  26. package/dist/inspector/index.js.map +1 -1
  27. package/dist/inspector/register-custom-element.js +1 -1
  28. package/dist/inspector/viewer/co-plain-text-view.d.ts +1 -1
  29. package/dist/inspector/viewer/co-plain-text-view.d.ts.map +1 -1
  30. package/dist/inspector/viewer/role-display.d.ts.map +1 -1
  31. package/dist/media/chunk-BBSS3NEY.js +211 -0
  32. package/dist/media/chunk-BBSS3NEY.js.map +1 -0
  33. package/dist/media/create-image.d.ts +48 -0
  34. package/dist/media/create-image.d.ts.map +1 -0
  35. package/dist/media/create-image.test.d.ts +2 -0
  36. package/dist/media/create-image.test.d.ts.map +1 -0
  37. package/dist/media/index.browser.d.ts +15 -0
  38. package/dist/media/index.browser.d.ts.map +1 -0
  39. package/dist/media/index.browser.js +113 -0
  40. package/dist/media/index.browser.js.map +1 -0
  41. package/dist/media/index.d.ts +53 -0
  42. package/dist/media/index.d.ts.map +1 -0
  43. package/dist/media/index.js +13 -0
  44. package/dist/media/index.js.map +1 -0
  45. package/dist/media/index.native.d.ts +17 -0
  46. package/dist/media/index.native.d.ts.map +1 -0
  47. package/dist/media/index.native.js +126 -0
  48. package/dist/media/index.native.js.map +1 -0
  49. package/dist/media/utils.d.ts +17 -0
  50. package/dist/media/utils.d.ts.map +1 -0
  51. package/dist/media/utils.test.d.ts +2 -0
  52. package/dist/media/utils.test.d.ts.map +1 -0
  53. package/dist/react/index.d.ts +1 -2
  54. package/dist/react/index.d.ts.map +1 -1
  55. package/dist/react/index.js +176 -59
  56. package/dist/react/index.js.map +1 -1
  57. package/dist/react/media/image.d.ts +62 -0
  58. package/dist/react/media/image.d.ts.map +1 -0
  59. package/dist/react/ssr.d.ts.map +1 -1
  60. package/dist/react/ssr.js.map +1 -1
  61. package/dist/react/tests/media/image.test.d.ts +2 -0
  62. package/dist/react/tests/media/image.test.d.ts.map +1 -0
  63. package/dist/react/tests/testUtils.d.ts.map +1 -1
  64. package/dist/react-core/auth/PassphraseAuth.d.ts +1 -1
  65. package/dist/react-core/auth/PassphraseAuth.d.ts.map +1 -1
  66. package/dist/react-core/index.js +1 -3
  67. package/dist/react-core/index.js.map +1 -1
  68. package/dist/react-core/tests/testUtils.d.ts.map +1 -1
  69. package/dist/react-core/tests/useDemoAuth.test.d.ts +2 -0
  70. package/dist/react-core/tests/useDemoAuth.test.d.ts.map +1 -0
  71. package/dist/react-native-core/index.d.ts +1 -1
  72. package/dist/react-native-core/index.d.ts.map +1 -1
  73. package/dist/react-native-core/index.js +84 -66
  74. package/dist/react-native-core/index.js.map +1 -1
  75. package/dist/react-native-core/media/image.d.ts +93 -0
  76. package/dist/react-native-core/media/image.d.ts.map +1 -0
  77. package/dist/react-native-core/testing.d.ts +2 -0
  78. package/dist/react-native-core/testing.d.ts.map +1 -0
  79. package/dist/svelte/index.d.ts +1 -0
  80. package/dist/svelte/index.d.ts.map +1 -1
  81. package/dist/svelte/index.js +1 -0
  82. package/dist/svelte/media/image.svelte +131 -0
  83. package/dist/svelte/media/image.svelte.d.ts +10 -0
  84. package/dist/svelte/media/image.svelte.d.ts.map +1 -0
  85. package/dist/svelte/media/index.d.ts +2 -0
  86. package/dist/svelte/media/index.d.ts.map +1 -0
  87. package/dist/svelte/media/index.js +1 -0
  88. package/dist/svelte/tests/media/image.svelte.test.d.ts +2 -0
  89. package/dist/svelte/tests/media/image.svelte.test.d.ts.map +1 -0
  90. package/dist/svelte/tests/media/image.svelte.test.js +430 -0
  91. package/dist/svelte/tests/testUtils.d.ts +11 -0
  92. package/dist/svelte/tests/testUtils.d.ts.map +1 -0
  93. package/dist/svelte/tests/testUtils.js +17 -0
  94. package/dist/svelte/tests/types.d.ts +3 -0
  95. package/dist/testing.js +1 -1
  96. package/dist/testing.js.map +1 -1
  97. package/dist/tools/coValues/coFeed.d.ts +15 -0
  98. package/dist/tools/coValues/coFeed.d.ts.map +1 -1
  99. package/dist/tools/coValues/deepLoading.d.ts +10 -10
  100. package/dist/tools/coValues/deepLoading.d.ts.map +1 -1
  101. package/dist/tools/coValues/extensions/imageDef.d.ts +3 -9
  102. package/dist/tools/coValues/extensions/imageDef.d.ts.map +1 -1
  103. package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts +1 -0
  104. package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts.map +1 -1
  105. package/dist/tools/index.d.ts +1 -1
  106. package/dist/tools/index.d.ts.map +1 -1
  107. package/dist/tools/testing.d.ts.map +1 -1
  108. package/package.json +12 -12
  109. package/src/inspector/viewer/co-plain-text-view.tsx +1 -5
  110. package/src/inspector/viewer/co-stream-view.tsx +1 -1
  111. package/src/inspector/viewer/role-display.tsx +4 -1
  112. package/src/{browser-media-images/index.test.browser.ts → media/create-image.test.ts} +146 -24
  113. package/src/media/create-image.ts +180 -0
  114. package/src/media/index.browser.ts +150 -0
  115. package/src/media/index.native.ts +153 -0
  116. package/src/media/index.ts +61 -0
  117. package/src/media/utils.test.ts +327 -0
  118. package/src/media/utils.ts +202 -0
  119. package/src/react/index.ts +1 -2
  120. package/src/react/media/image.tsx +210 -0
  121. package/src/react/ssr.ts +1 -3
  122. package/src/react/tests/media/image.test.tsx +588 -0
  123. package/src/react/tests/testUtils.tsx +2 -10
  124. package/src/react-core/auth/PassphraseAuth.tsx +1 -5
  125. package/src/react-core/tests/testUtils.tsx +2 -10
  126. package/src/react-native-core/index.ts +1 -1
  127. package/src/react-native-core/media/image.tsx +159 -0
  128. package/src/svelte/index.ts +1 -0
  129. package/src/svelte/media/image.svelte +131 -0
  130. package/src/svelte/media/index.ts +1 -0
  131. package/src/svelte/tests/media/image.svelte.test.ts +583 -0
  132. package/src/svelte/tests/testUtils.ts +33 -0
  133. package/src/svelte/tests/types.d.ts +3 -0
  134. package/src/tools/coValues/coFeed.ts +40 -7
  135. package/src/tools/coValues/deepLoading.ts +46 -32
  136. package/src/tools/coValues/extensions/imageDef.ts +3 -49
  137. package/src/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.ts +6 -0
  138. package/src/tools/index.ts +0 -1
  139. package/src/tools/testing.ts +3 -1
  140. package/src/tools/tests/coList.test.ts +1 -1
  141. package/src/tools/tests/coMap.record.test-d.ts +105 -0
  142. package/src/tools/tests/coMap.record.test.ts +48 -2
  143. package/src/tools/tests/coMap.test-d.ts +50 -0
  144. package/src/tools/tests/coOptional.test.ts +3 -1
  145. package/tsconfig.json +1 -0
  146. package/tsup.config.ts +4 -9
  147. package/vitest.config.ts +14 -21
  148. package/dist/browser-media-images/index.d.ts +0 -9
  149. package/dist/browser-media-images/index.d.ts.map +0 -1
  150. package/dist/browser-media-images/index.js +0 -72
  151. package/dist/browser-media-images/index.js.map +0 -1
  152. package/dist/browser-media-images/index.test.browser.d.ts +0 -2
  153. package/dist/browser-media-images/index.test.browser.d.ts.map +0 -1
  154. package/dist/chunk-H3BIFFQG.js.map +0 -1
  155. package/dist/react/media.d.ts +0 -24
  156. package/dist/react/media.d.ts.map +0 -1
  157. package/dist/react-native-core/media.d.ts +0 -24
  158. package/dist/react-native-core/media.d.ts.map +0 -1
  159. package/dist/react-native-media-images/index.d.ts +0 -7
  160. package/dist/react-native-media-images/index.d.ts.map +0 -1
  161. package/dist/react-native-media-images/index.js +0 -177
  162. package/dist/react-native-media-images/index.js.map +0 -1
  163. package/dist/tools/tests/imageDef.test.d.ts +0 -2
  164. package/dist/tools/tests/imageDef.test.d.ts.map +0 -1
  165. package/src/browser-media-images/index.ts +0 -131
  166. package/src/react/media.tsx +0 -74
  167. package/src/react/scratch.tsx +0 -50
  168. package/src/react-native-core/media.tsx +0 -79
  169. package/src/react-native-media-images/index.ts +0 -238
  170. package/src/tools/tests/imageDef.test.ts +0 -278
@@ -0,0 +1,583 @@
1
+ // @vitest-environment happy-dom
2
+ import { FileStream, ImageDefinition } from "jazz-tools";
3
+ import { createJazzTestAccount } from "jazz-tools/testing";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import Image from "../../media/image.svelte";
6
+ import { render, screen, waitFor } from "../testUtils";
7
+
8
+ describe("Image", async () => {
9
+ const account = await createJazzTestAccount({
10
+ isCurrentActiveAccount: true,
11
+ });
12
+
13
+ const renderWithAccount = (props: any) => render(Image, props, { account });
14
+
15
+ describe("initial rendering", () => {
16
+ it("should render nothing if coValue is not found", async () => {
17
+ const { container } = renderWithAccount({
18
+ imageId: "co_zMTubMby3QiKDYnW9e2BEXW7Xaq",
19
+ alt: "test",
20
+ });
21
+
22
+ const img = container.querySelector("img");
23
+ expect(img).toBeDefined();
24
+ expect(img!.getAttribute("width")).toBe(null);
25
+ expect(img!.getAttribute("height")).toBe(null);
26
+ expect(img!.alt).toBe("test");
27
+ expect(img!.src).toBe("");
28
+ });
29
+
30
+ it("should render an empty image if the image is not loaded yet", async () => {
31
+ const original = FileStream.create({ owner: account._owner });
32
+ original.start({ mimeType: "image/jpeg" });
33
+ // Don't end original, so it has no chunks
34
+
35
+ const im = ImageDefinition.create(
36
+ {
37
+ original,
38
+ originalSize: [100, 100],
39
+ progressive: false,
40
+ },
41
+ {
42
+ owner: account,
43
+ },
44
+ );
45
+
46
+ const { container } = renderWithAccount({
47
+ imageId: im.id,
48
+ alt: "test",
49
+ });
50
+
51
+ const img = container.querySelector("img");
52
+ expect(img).toBeDefined();
53
+ expect(img!.getAttribute("width")).toBe(null);
54
+ expect(img!.getAttribute("height")).toBe(null);
55
+ expect(img!.alt).toBe("test");
56
+ expect(img!.src).toBe("");
57
+ });
58
+
59
+ it("should render the placeholder image if the image is not loaded yet", async () => {
60
+ const placeholderDataUrl =
61
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=";
62
+
63
+ const original = FileStream.create({ owner: account._owner });
64
+ original.start({ mimeType: "image/jpeg" });
65
+ // Don't end original, so it has no chunks
66
+
67
+ const im = ImageDefinition.create(
68
+ {
69
+ original,
70
+ originalSize: [100, 100],
71
+ progressive: false,
72
+ placeholderDataURL: placeholderDataUrl,
73
+ },
74
+ {
75
+ owner: account,
76
+ },
77
+ );
78
+
79
+ const { container } = renderWithAccount({
80
+ imageId: im.id,
81
+ alt: "test",
82
+ });
83
+
84
+ const img = container.querySelector("img");
85
+ expect(img).toBeDefined();
86
+ expect(img!.src).toBe(placeholderDataUrl);
87
+ });
88
+
89
+ it("should render the original image once loaded", async () => {
90
+ const createObjectURLSpy = vi
91
+ .spyOn(URL, "createObjectURL")
92
+ .mockImplementation((blob) => {
93
+ if (!(blob instanceof Blob)) {
94
+ throw new Error("Blob expected");
95
+ }
96
+ return `blob:test-${blob.size}`;
97
+ });
98
+
99
+ const im = ImageDefinition.create(
100
+ {
101
+ original: await createDummyFileStream(100, account),
102
+ originalSize: [100, 100],
103
+ progressive: false,
104
+ },
105
+ {
106
+ owner: account,
107
+ },
108
+ );
109
+
110
+ renderWithAccount({
111
+ imageId: im.id,
112
+ alt: "test-loading",
113
+ });
114
+
115
+ await waitFor(() => {
116
+ expect(
117
+ (screen.getByAltText("test-loading") as HTMLImageElement).src,
118
+ ).toBe("blob:test-100");
119
+ });
120
+
121
+ expect(createObjectURLSpy).toHaveBeenCalledOnce();
122
+ });
123
+ });
124
+
125
+ describe("dimensions", () => {
126
+ it("should render the original image if the width and height are not set", async () => {
127
+ const im = ImageDefinition.create(
128
+ {
129
+ original: await createDummyFileStream(100, account),
130
+ originalSize: [100, 100],
131
+ progressive: false,
132
+ },
133
+ {
134
+ owner: account,
135
+ },
136
+ );
137
+
138
+ const { container } = renderWithAccount({
139
+ imageId: im.id,
140
+ alt: "test",
141
+ });
142
+
143
+ const img = container.querySelector("img");
144
+ expect(img).toBeDefined();
145
+ expect(img!.getAttribute("width")).toBe(null);
146
+ expect(img!.getAttribute("height")).toBe(null);
147
+ });
148
+
149
+ it("should render the original sizes", async () => {
150
+ const im = ImageDefinition.create(
151
+ {
152
+ original: await createDummyFileStream(100, account),
153
+ originalSize: [100, 100],
154
+ progressive: false,
155
+ },
156
+ {
157
+ owner: account,
158
+ },
159
+ );
160
+
161
+ const { container } = renderWithAccount({
162
+ imageId: im.id,
163
+ alt: "test",
164
+ width: "original",
165
+ height: "original",
166
+ });
167
+
168
+ const img = container.querySelector("img");
169
+ expect(img).toBeDefined();
170
+ expect(img!.getAttribute("width")).toBe("100");
171
+ expect(img!.getAttribute("height")).toBe("100");
172
+ });
173
+
174
+ it("should render the original size keeping the aspect ratio", async () => {
175
+ const im = ImageDefinition.create(
176
+ {
177
+ original: await createDummyFileStream(100, account),
178
+ originalSize: [100, 100],
179
+ progressive: false,
180
+ },
181
+ {
182
+ owner: account,
183
+ },
184
+ );
185
+
186
+ const { container } = renderWithAccount({
187
+ imageId: im.id,
188
+ alt: "test",
189
+ width: "original",
190
+ height: 300,
191
+ });
192
+
193
+ const img = container.querySelector("img");
194
+ expect(img).toBeDefined();
195
+ expect(img!.getAttribute("width")).toBe("300");
196
+ expect(img!.getAttribute("height")).toBe("300");
197
+ });
198
+
199
+ it("should render the width attribute if it is set", async () => {
200
+ const im = ImageDefinition.create(
201
+ {
202
+ original: await createDummyFileStream(100, account),
203
+ originalSize: [100, 100],
204
+ progressive: false,
205
+ },
206
+ {
207
+ owner: account,
208
+ },
209
+ );
210
+
211
+ const { container } = renderWithAccount({
212
+ imageId: im.id,
213
+ alt: "test",
214
+ width: 50,
215
+ });
216
+
217
+ const img = container.querySelector("img");
218
+ expect(img).toBeDefined();
219
+ expect(img!.getAttribute("width")).toBe("50");
220
+ expect(img!.getAttribute("height")).toBeNull();
221
+ });
222
+
223
+ it("should render the height attribute if it is set", async () => {
224
+ const im = ImageDefinition.create(
225
+ {
226
+ original: await createDummyFileStream(100, account),
227
+ originalSize: [100, 100],
228
+ progressive: false,
229
+ },
230
+ {
231
+ owner: account,
232
+ },
233
+ );
234
+
235
+ const { container } = renderWithAccount({
236
+ imageId: im.id,
237
+ alt: "test",
238
+ height: 50,
239
+ });
240
+
241
+ const img = container.querySelector("img");
242
+ expect(img).toBeDefined();
243
+ expect(img!.getAttribute("width")).toBeNull();
244
+ expect(img!.getAttribute("height")).toBe("50");
245
+ });
246
+
247
+ it("should render the class attribute if it is set", async () => {
248
+ const im = ImageDefinition.create(
249
+ {
250
+ original: await createDummyFileStream(100, account),
251
+ originalSize: [100, 100],
252
+ progressive: false,
253
+ },
254
+ {
255
+ owner: account,
256
+ },
257
+ );
258
+
259
+ const { container } = renderWithAccount({
260
+ imageId: im.id,
261
+ alt: "test",
262
+ class: "test-class",
263
+ });
264
+
265
+ const img = container.querySelector("img");
266
+ expect(img).toBeDefined();
267
+ expect(img!.classList.contains("test-class")).toBe(true);
268
+ });
269
+ });
270
+
271
+ describe("progressive loading", () => {
272
+ it("should render the resized image if progressive loading is enabled", async () => {
273
+ const createObjectURLSpy = vi
274
+ .spyOn(URL, "createObjectURL")
275
+ .mockImplementation((blob) => {
276
+ if (!(blob instanceof Blob)) {
277
+ throw new Error("Blob expected");
278
+ }
279
+ return `blob:test-${blob.size}`;
280
+ });
281
+
282
+ const original = await createDummyFileStream(500, account);
283
+
284
+ const im = ImageDefinition.create(
285
+ {
286
+ original,
287
+ originalSize: [500, 500],
288
+ progressive: true,
289
+ },
290
+ {
291
+ owner: account,
292
+ },
293
+ );
294
+
295
+ im["500x500"] = original;
296
+ im["256x256"] = await createDummyFileStream(256, account);
297
+
298
+ const { container } = renderWithAccount({
299
+ imageId: im.id,
300
+ alt: "test-progressive",
301
+ width: 300,
302
+ });
303
+
304
+ await waitFor(() => {
305
+ expect((container.querySelector("img") as HTMLImageElement).src).toBe(
306
+ "blob:test-500",
307
+ );
308
+ });
309
+
310
+ expect(createObjectURLSpy).toHaveBeenCalledOnce();
311
+ });
312
+
313
+ it("should show the highest resolution images as they are loaded", async () => {
314
+ const createObjectURLSpy = vi
315
+ .spyOn(URL, "createObjectURL")
316
+ .mockImplementation((blob) => {
317
+ if (!(blob instanceof Blob)) {
318
+ throw new Error("Blob expected");
319
+ }
320
+ return `blob:test-${blob.size}`;
321
+ });
322
+
323
+ const original = await createDummyFileStream(1920, account);
324
+
325
+ const im = ImageDefinition.create(
326
+ {
327
+ original,
328
+ originalSize: [1920, 1080],
329
+ progressive: true,
330
+ },
331
+ {
332
+ owner: account,
333
+ },
334
+ );
335
+
336
+ im["1920x1080"] = original;
337
+ im["256x256"] = await createDummyFileStream(256, account);
338
+
339
+ const { container } = renderWithAccount({
340
+ imageId: im.id,
341
+ alt: "test-progressive",
342
+ width: 1024,
343
+ });
344
+
345
+ await waitFor(() => {
346
+ expect((container.querySelector("img") as HTMLImageElement).src).toBe(
347
+ "blob:test-1920",
348
+ );
349
+ });
350
+
351
+ expect(createObjectURLSpy).toHaveBeenCalledTimes(1);
352
+
353
+ // Load higher resolution image
354
+ im["1024x1024"] = await createDummyFileStream(1024, account);
355
+
356
+ await waitFor(() => {
357
+ expect((container.querySelector("img") as HTMLImageElement).src).toBe(
358
+ "blob:test-1024",
359
+ );
360
+ });
361
+
362
+ expect(createObjectURLSpy).toHaveBeenCalledTimes(2);
363
+ });
364
+
365
+ it("should show the best loaded resolution if width is set", async () => {
366
+ const createObjectURLSpy = vi
367
+ .spyOn(URL, "createObjectURL")
368
+ .mockImplementation((blob) => {
369
+ if (!(blob instanceof Blob)) {
370
+ throw new Error("Blob expected");
371
+ }
372
+ return `blob:test-${blob.size}`;
373
+ });
374
+
375
+ const original = await FileStream.createFromBlob(createDummyBlob(1), {
376
+ owner: account,
377
+ });
378
+
379
+ const im = ImageDefinition.create(
380
+ {
381
+ original,
382
+ originalSize: [100, 100],
383
+ progressive: true,
384
+ },
385
+ {
386
+ owner: account,
387
+ },
388
+ );
389
+
390
+ im["100x100"] = original;
391
+ im["256x256"] = await createDummyFileStream(256, account);
392
+ im["1024x1024"] = await createDummyFileStream(1024, account);
393
+
394
+ const { container } = renderWithAccount({
395
+ imageId: im.id,
396
+ alt: "test-progressive",
397
+ width: 256,
398
+ });
399
+
400
+ await waitFor(() => {
401
+ expect((container.querySelector("img") as HTMLImageElement).src).toBe(
402
+ "blob:test-256",
403
+ );
404
+ });
405
+
406
+ expect(createObjectURLSpy).toHaveBeenCalledTimes(1);
407
+ });
408
+
409
+ it("should show the original image if asked resolution matches", async () => {
410
+ const createObjectURLSpy = vi
411
+ .spyOn(URL, "createObjectURL")
412
+ .mockImplementation((blob) => {
413
+ if (!(blob instanceof Blob)) {
414
+ throw new Error("Blob expected");
415
+ }
416
+ return `blob:test-${blob.size}`;
417
+ });
418
+
419
+ const original = await createDummyFileStream(100, account);
420
+ const im = ImageDefinition.create(
421
+ {
422
+ original,
423
+ originalSize: [100, 100],
424
+ progressive: true,
425
+ },
426
+ {
427
+ owner: account,
428
+ },
429
+ );
430
+
431
+ im["100x100"] = original;
432
+ im["256x256"] = await createDummyFileStream(256, account);
433
+
434
+ const { container } = renderWithAccount({
435
+ imageId: im.id,
436
+ alt: "test-progressive",
437
+ width: 100,
438
+ });
439
+
440
+ await waitFor(() => {
441
+ expect((container.querySelector("img") as HTMLImageElement).src).toBe(
442
+ "blob:test-100",
443
+ );
444
+ });
445
+
446
+ expect(createObjectURLSpy).toHaveBeenCalledTimes(1);
447
+ });
448
+
449
+ it("should update to a higher resolution image when width/height props are changed at runtime", async () => {
450
+ const createObjectURLSpy = vi
451
+ .spyOn(URL, "createObjectURL")
452
+ .mockImplementation((blob) => {
453
+ if (!(blob instanceof Blob)) {
454
+ throw new Error("Blob expected");
455
+ }
456
+ return `blob:test-${blob.size}`;
457
+ });
458
+
459
+ const original = await createDummyFileStream(256, account);
460
+ const im = ImageDefinition.create(
461
+ {
462
+ original,
463
+ originalSize: [256, 256],
464
+ progressive: true,
465
+ },
466
+ {
467
+ owner: account,
468
+ },
469
+ );
470
+ im["256x256"] = original;
471
+ im["1024x1024"] = await createDummyFileStream(1024, account);
472
+
473
+ const { container, rerender } = renderWithAccount({
474
+ imageId: im.id,
475
+ alt: "test-dynamic",
476
+ width: 256,
477
+ height: 256,
478
+ });
479
+
480
+ // Initially, should load 256x256
481
+ await waitFor(() => {
482
+ expect((container.querySelector("img") as HTMLImageElement).src).toBe(
483
+ "blob:test-256",
484
+ );
485
+ });
486
+ expect(createObjectURLSpy).toHaveBeenCalledTimes(1);
487
+
488
+ rerender({
489
+ imageId: im.id,
490
+ alt: "test-dynamic",
491
+ width: 1024,
492
+ height: 1024,
493
+ });
494
+
495
+ // After prop change, should load 1024x1024
496
+ await waitFor(() => {
497
+ expect((container.querySelector("img") as HTMLImageElement).src).toBe(
498
+ "blob:test-1024",
499
+ );
500
+ });
501
+ expect(createObjectURLSpy).toHaveBeenCalledTimes(2);
502
+ });
503
+ });
504
+
505
+ describe("lazy loading", () => {
506
+ it("should return an empty png if loading is lazy and placeholder is not set", async () => {
507
+ const im = ImageDefinition.create(
508
+ {
509
+ original: await createDummyFileStream(100, account),
510
+ originalSize: [100, 100],
511
+ progressive: false,
512
+ },
513
+ {
514
+ owner: account,
515
+ },
516
+ );
517
+
518
+ const { container } = renderWithAccount({
519
+ imageId: im.id,
520
+ alt: "test",
521
+ loading: "lazy",
522
+ });
523
+
524
+ const img = container.querySelector("img");
525
+ expect(img).toBeDefined();
526
+ expect(img!.src).toBe("blob:test-70");
527
+ });
528
+
529
+ it("should load the image when threshold is reached", async () => {
530
+ const createObjectURLSpy = vi
531
+ .spyOn(URL, "createObjectURL")
532
+ .mockImplementation((blob) => {
533
+ if (!(blob instanceof Blob)) {
534
+ throw new Error("Blob expected");
535
+ }
536
+ return `blob:test-${blob.size}`;
537
+ });
538
+
539
+ const im = ImageDefinition.create(
540
+ {
541
+ original: await createDummyFileStream(100, account),
542
+ originalSize: [100, 100],
543
+ progressive: false,
544
+ },
545
+ {
546
+ owner: account,
547
+ },
548
+ );
549
+
550
+ const { container } = renderWithAccount({
551
+ imageId: im.id,
552
+ alt: "test",
553
+ loading: "lazy",
554
+ });
555
+
556
+ const img = container.querySelector("img");
557
+ // simulate the load event when the browser's viewport reach the image
558
+ img!.dispatchEvent(new Event("load"));
559
+
560
+ await waitFor(() => {
561
+ expect((container.querySelector("img") as HTMLImageElement).src).toBe(
562
+ "blob:test-100",
563
+ );
564
+ });
565
+
566
+ expect(createObjectURLSpy).toHaveBeenCalledTimes(2);
567
+ });
568
+ });
569
+ });
570
+
571
+ function createDummyBlob(size: number): Blob {
572
+ const blob = new Blob([new Uint8Array(size)], { type: "image/png" });
573
+ return blob;
574
+ }
575
+
576
+ function createDummyFileStream(
577
+ size: number,
578
+ account: Awaited<ReturnType<typeof createJazzTestAccount>>,
579
+ ) {
580
+ return FileStream.createFromBlob(createDummyBlob(size), {
581
+ owner: account,
582
+ });
583
+ }
@@ -0,0 +1,33 @@
1
+ import { render as renderSvelte } from "@testing-library/svelte";
2
+ import { Account, AnonymousJazzAgent } from "jazz-tools";
3
+ import { TestJazzContextManager } from "jazz-tools/testing";
4
+ import { Component, ComponentProps } from "svelte";
5
+ import { JAZZ_AUTH_CTX, JAZZ_CTX } from "../jazz.svelte";
6
+
7
+ type JazzExtendedOptions = {
8
+ account: Account | { guest: AnonymousJazzAgent };
9
+ };
10
+
11
+ const render = <T extends Component>(
12
+ component: T,
13
+ props: ComponentProps<T>,
14
+ jazzOptions: JazzExtendedOptions,
15
+ ) => {
16
+ const ctx = TestJazzContextManager.fromAccountOrGuest(jazzOptions.account);
17
+
18
+ return renderSvelte(
19
+ // @ts-expect-error Svelte new Component type is not compatible with @testing-library/svelte
20
+ component,
21
+ {
22
+ props,
23
+ context: new Map<any, any>([
24
+ [JAZZ_CTX, { current: ctx.getCurrentValue() }],
25
+ [JAZZ_AUTH_CTX, ctx.getAuthSecretStorage()],
26
+ ]),
27
+ },
28
+ );
29
+ };
30
+
31
+ export * from "@testing-library/svelte";
32
+
33
+ export { render };
@@ -0,0 +1,3 @@
1
+ declare module "*.svelte" {
2
+ export { SvelteComponentDev as default } from "svelte/internal";
3
+ }
@@ -9,7 +9,7 @@ import type {
9
9
  RawCoStream,
10
10
  SessionID,
11
11
  } from "cojson";
12
- import { MAX_RECOMMENDED_TX_SIZE, cojsonInternals } from "cojson";
12
+ import { cojsonInternals } from "cojson";
13
13
  import type {
14
14
  AnonymousJazzAgent,
15
15
  CoValue,
@@ -836,6 +836,38 @@ export class FileStream extends CoValueBase implements CoValue {
836
836
  }
837
837
  | Account
838
838
  | Group,
839
+ ): Promise<FileStream> {
840
+ const arrayBuffer = await blob.arrayBuffer();
841
+ return this.createFromArrayBuffer(
842
+ arrayBuffer,
843
+ blob.type,
844
+ blob instanceof File ? blob.name : undefined,
845
+ options,
846
+ );
847
+ }
848
+
849
+ /**
850
+ * Create a `FileStream` from a `Blob` or `File`
851
+ *
852
+ * @example
853
+ * ```ts
854
+ * import { coField, FileStream } from "jazz-tools";
855
+ *
856
+ * const fileStream = await FileStream.createFromBlob(file, {owner: group})
857
+ * ```
858
+ * @category Content
859
+ */
860
+ static async createFromArrayBuffer(
861
+ arrayBuffer: ArrayBuffer,
862
+ mimeType: string,
863
+ fileName: string | undefined,
864
+ options?:
865
+ | {
866
+ owner?: Group | Account;
867
+ onProgress?: (progress: number) => void;
868
+ }
869
+ | Account
870
+ | Group,
839
871
  ): Promise<FileStream> {
840
872
  const stream = this.create(options);
841
873
  const onProgress =
@@ -843,13 +875,14 @@ export class FileStream extends CoValueBase implements CoValue {
843
875
 
844
876
  const start = Date.now();
845
877
 
846
- const data = new Uint8Array(await blob.arrayBuffer());
878
+ const data = new Uint8Array(arrayBuffer);
847
879
  stream.start({
848
- mimeType: blob.type,
849
- totalSizeBytes: blob.size,
850
- fileName: blob instanceof File ? blob.name : undefined,
880
+ mimeType,
881
+ totalSizeBytes: arrayBuffer.byteLength,
882
+ fileName,
851
883
  });
852
- const chunkSize = MAX_RECOMMENDED_TX_SIZE;
884
+ const chunkSize =
885
+ cojsonInternals.TRANSACTION_CONFIG.MAX_RECOMMENDED_TX_SIZE;
853
886
 
854
887
  let lastProgressUpdate = Date.now();
855
888
 
@@ -870,7 +903,7 @@ export class FileStream extends CoValueBase implements CoValue {
870
903
  "Finished creating binary stream in",
871
904
  (end - start) / 1000,
872
905
  "s - Throughput in MB/s",
873
- (1000 * (blob.size / (end - start))) / (1024 * 1024),
906
+ (1000 * (arrayBuffer.byteLength / (end - start))) / (1024 * 1024),
874
907
  );
875
908
  onProgress?.(1);
876
909