@gradio/image 0.26.0 → 0.26.2

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # @gradio/image
2
2
 
3
+ ## 0.26.2
4
+
5
+ ### Fixes
6
+
7
+ - [#13181](https://github.com/gradio-app/gradio/pull/13181) [`755c3d3`](https://github.com/gradio-app/gradio/commit/755c3d32c388a36d2296f8d895c5c0e1144fb54f) - fix: show validation errors on StatusTracker-dependent components. Thanks @hysts!
8
+
9
+ ### Dependency updates
10
+
11
+ - @gradio/atoms@0.23.1
12
+ - @gradio/statustracker@0.14.0
13
+ - @gradio/client@2.2.0
14
+
15
+ ## 0.26.1
16
+
17
+ ### Fixes
18
+
19
+ - [#13165](https://github.com/gradio-app/gradio/pull/13165) [`1a0e277`](https://github.com/gradio-app/gradio/commit/1a0e2770067789ba6ec5646e473e1df183cd7183) - Use test utils. Thanks @freddyaboulton!
20
+
21
+ ### Dependency updates
22
+
23
+ - @gradio/utils@0.12.2
24
+ - @gradio/atoms@0.23.0
25
+ - @gradio/statustracker@0.13.1
26
+ - @gradio/upload@0.17.8
27
+
3
28
  ## 0.26.0
4
29
 
5
30
  ### Features
package/Image.test.ts ADDED
@@ -0,0 +1,719 @@
1
+ import { test, describe, afterEach, expect, vi } from "vitest";
2
+ import {
3
+ cleanup,
4
+ render,
5
+ fireEvent,
6
+ waitFor,
7
+ upload_file,
8
+ drop_file,
9
+ mock_client,
10
+ download_file,
11
+ TEST_JPG,
12
+ TEST_PNG
13
+ } from "@self/tootils/render";
14
+ import { run_shared_prop_tests } from "@self/tootils/shared-prop-tests";
15
+
16
+ import Image from "./Index.svelte";
17
+ import { get_coordinates_of_clicked_image } from "./shared/utils";
18
+
19
+ const fake_value = {
20
+ path: "test.png",
21
+ url: "https://example.com/test.png",
22
+ orig_name: "test.png",
23
+ size: 1024,
24
+ mime_type: "image/png",
25
+ is_stream: false
26
+ };
27
+
28
+ const default_props = {
29
+ sources: ["upload", "webcam", "clipboard"] as (
30
+ | "upload"
31
+ | "webcam"
32
+ | "clipboard"
33
+ )[],
34
+ value: null as any,
35
+ label: "Image",
36
+ show_label: true,
37
+ interactive: true,
38
+ _selectable: false,
39
+ height: 300,
40
+ width: 300,
41
+ streaming: false,
42
+ stream_every: 1,
43
+ pending: false,
44
+ input_ready: true,
45
+ placeholder: "",
46
+ buttons: [] as (string | { value: string; id: number; icon: null })[],
47
+ webcam_options: { mirror: false, constraints: {} },
48
+ watermark: null
49
+ };
50
+
51
+ run_shared_prop_tests({
52
+ component: Image,
53
+ name: "Image",
54
+ base_props: {
55
+ ...default_props
56
+ },
57
+ has_label: false,
58
+ has_validation_error: true
59
+ });
60
+
61
+ describe("Image", () => {
62
+ afterEach(() => cleanup());
63
+
64
+ test("renders with null value showing upload area", async () => {
65
+ const { getByLabelText } = await render(Image, {
66
+ ...default_props,
67
+ value: null
68
+ });
69
+
70
+ expect(getByLabelText("image.drop_to_upload")).toBeVisible();
71
+ });
72
+
73
+ test("renders image when value is set", async () => {
74
+ const { container } = await render(Image, {
75
+ ...default_props,
76
+ value: fake_value
77
+ });
78
+
79
+ const img = container.querySelector("img");
80
+ expect(img).toBeTruthy();
81
+ expect(img?.getAttribute("src")).toBe("https://example.com/test.png");
82
+ });
83
+ });
84
+
85
+ describe("Props: label", () => {
86
+ afterEach(() => cleanup());
87
+
88
+ test("label text is rendered", async () => {
89
+ const result = await render(Image, {
90
+ ...default_props,
91
+ label: "My Custom Label",
92
+ show_label: true
93
+ });
94
+ const el = result.getByText("My Custom Label");
95
+ expect(el).toBeTruthy();
96
+ });
97
+
98
+ test("show_label: true makes the label visible", async () => {
99
+ const result = await render(Image, {
100
+ ...default_props,
101
+ label: "Visible Label",
102
+ show_label: true
103
+ });
104
+ const el = result.getByText("Visible Label");
105
+ expect(el).toBeVisible();
106
+ });
107
+
108
+ test("show_label: false hides the label visually but keeps it in the DOM", async () => {
109
+ const result = await render(Image, {
110
+ ...default_props,
111
+ label: "Hidden Label",
112
+ show_label: false
113
+ });
114
+ const el = result.getByText("Hidden Label");
115
+ expect(el).not.toBeVisible();
116
+ });
117
+ });
118
+
119
+ describe("Props: sources", () => {
120
+ afterEach(() => cleanup());
121
+
122
+ test("multiple sources renders source selection buttons", async () => {
123
+ const { getByTestId } = await render(Image, {
124
+ ...default_props,
125
+ sources: ["upload", "webcam", "clipboard"]
126
+ });
127
+
128
+ const sourceSelect = getByTestId("source-select");
129
+ expect(sourceSelect).toBeTruthy();
130
+ });
131
+
132
+ test("single upload source does not render source selection", async () => {
133
+ const { queryByTestId, getByLabelText } = await render(Image, {
134
+ ...default_props,
135
+ sources: ["upload"]
136
+ });
137
+ expect(getByLabelText("image.drop_to_upload")).toBeVisible();
138
+ const sourceSelect = queryByTestId("source-select");
139
+ expect(sourceSelect).toBeNull();
140
+ });
141
+
142
+ test("single clipboard source does render source selection", async () => {
143
+ const { queryByTestId, getByLabelText } = await render(Image, {
144
+ ...default_props,
145
+ sources: ["clipboard"]
146
+ });
147
+ expect(getByLabelText("Paste from clipboard")).toBeTruthy();
148
+ const sourceSelect = queryByTestId("source-select");
149
+ expect(sourceSelect).not.toBeNull();
150
+ });
151
+
152
+ test("clipboard and upload sources render paste and upload buttons", async () => {
153
+ const { getByLabelText } = await render(Image, {
154
+ ...default_props,
155
+ sources: ["upload", "clipboard"]
156
+ });
157
+
158
+ expect(getByLabelText("Upload file")).toBeTruthy();
159
+ expect(getByLabelText("Paste from clipboard")).toBeTruthy();
160
+ });
161
+
162
+ test("upload and webcam sources render corresponding buttons", async () => {
163
+ const { getByLabelText } = await render(Image, {
164
+ ...default_props,
165
+ sources: ["upload", "webcam"]
166
+ });
167
+
168
+ expect(getByLabelText("Upload file")).toBeTruthy();
169
+ expect(getByLabelText("Capture from camera")).toBeTruthy();
170
+ });
171
+
172
+ test("clicking webcam source button hides the upload area", async () => {
173
+ const { getByLabelText } = await render(Image, {
174
+ ...default_props,
175
+ sources: ["upload", "webcam"]
176
+ });
177
+
178
+ expect(getByLabelText("image.drop_to_upload")).toBeVisible();
179
+
180
+ await fireEvent.click(getByLabelText("Capture from camera"));
181
+
182
+ // Re-query after click to avoid stale references from potential rerenders
183
+ expect(getByLabelText("image.drop_to_upload")).not.toBeVisible();
184
+ });
185
+
186
+ test("clicking upload source button shows the upload area again", async () => {
187
+ const { getByLabelText } = await render(Image, {
188
+ ...default_props,
189
+ sources: ["upload", "webcam"]
190
+ });
191
+
192
+ await fireEvent.click(getByLabelText("Capture from camera"));
193
+ expect(getByLabelText("image.drop_to_upload")).not.toBeVisible();
194
+
195
+ await fireEvent.click(getByLabelText("Upload file"));
196
+ expect(getByLabelText("image.drop_to_upload")).toBeVisible();
197
+ });
198
+ });
199
+
200
+ describe("Props: interactive", () => {
201
+ afterEach(() => cleanup());
202
+
203
+ test("interactive=true shows an upload area when value is null", async () => {
204
+ const { getByLabelText } = await render(Image, {
205
+ ...default_props,
206
+ interactive: true,
207
+ value: null
208
+ });
209
+
210
+ expect(getByLabelText("image.drop_to_upload")).toBeTruthy();
211
+ });
212
+
213
+ test("interactive=false renders the image without upload controls", async () => {
214
+ const { container, queryByLabelText } = await render(Image, {
215
+ ...default_props,
216
+ interactive: false,
217
+ value: fake_value,
218
+ buttons: ["fullscreen"]
219
+ });
220
+
221
+ const img = container.querySelector("img");
222
+ expect(img).toBeTruthy();
223
+ // No upload area or source selection in static mode
224
+ expect(queryByLabelText("image.drop_to_upload")).toBeNull();
225
+ expect(queryByLabelText("Upload file")).toBeNull();
226
+ });
227
+
228
+ test("interactive=false with null value does not show upload area", async () => {
229
+ const { queryByLabelText } = await render(Image, {
230
+ ...default_props,
231
+ interactive: false,
232
+ value: null
233
+ });
234
+
235
+ expect(queryByLabelText("image.drop_to_upload")).toBeNull();
236
+ });
237
+ });
238
+
239
+ describe("Events: change", () => {
240
+ afterEach(() => cleanup());
241
+
242
+ test("setting value triggers change event", async () => {
243
+ const { listen, set_data } = await render(Image, {
244
+ ...default_props,
245
+ value: null
246
+ });
247
+
248
+ const change = listen("change");
249
+
250
+ await set_data({ value: fake_value });
251
+
252
+ expect(change).toHaveBeenCalledTimes(1);
253
+ });
254
+
255
+ test("change event is not triggered on mount with a default value", async () => {
256
+ const { listen } = await render(Image, {
257
+ ...default_props,
258
+ value: fake_value
259
+ });
260
+
261
+ const change = listen("change", { retrospective: true });
262
+
263
+ expect(change).not.toHaveBeenCalled();
264
+ });
265
+
266
+ test("changing value multiple times triggers change each time", async () => {
267
+ const { listen, set_data } = await render(Image, {
268
+ ...default_props,
269
+ value: null
270
+ });
271
+
272
+ const change = listen("change");
273
+
274
+ const value_a = { ...fake_value, url: "https://example.com/a.png" };
275
+ const value_b = { ...fake_value, url: "https://example.com/b.png" };
276
+
277
+ await set_data({ value: value_a });
278
+ await set_data({ value: value_b });
279
+
280
+ expect(change).toHaveBeenCalledTimes(2);
281
+ });
282
+
283
+ test("setting value to null after a value triggers change", async () => {
284
+ const { listen, set_data } = await render(Image, {
285
+ ...default_props,
286
+ value: fake_value
287
+ });
288
+
289
+ const change = listen("change");
290
+
291
+ await set_data({ value: null });
292
+
293
+ expect(change).toHaveBeenCalledTimes(1);
294
+ });
295
+ });
296
+
297
+ describe("Props: buttons (static mode)", () => {
298
+ afterEach(() => cleanup());
299
+
300
+ test("buttons with download shows download link", async () => {
301
+ const { container } = await render(Image, {
302
+ ...default_props,
303
+ interactive: false,
304
+ value: {
305
+ ...TEST_JPG,
306
+ is_stream: false
307
+ },
308
+ buttons: ["download"]
309
+ });
310
+
311
+ const downloadLink = container.querySelector("a.download-link");
312
+ expect(downloadLink).toBeTruthy();
313
+
314
+ const { suggested_filename } = await download_file("a.download-link");
315
+ expect(suggested_filename).toBe("cheetah1.jpg");
316
+ });
317
+
318
+ test("buttons with fullscreen shows fullscreen button", async () => {
319
+ const { getByLabelText } = await render(Image, {
320
+ ...default_props,
321
+ interactive: false,
322
+ value: fake_value,
323
+ buttons: ["fullscreen"]
324
+ });
325
+
326
+ expect(getByLabelText("Fullscreen")).toBeTruthy();
327
+ });
328
+
329
+ test("empty buttons array shows no action buttons", async () => {
330
+ const { queryByLabelText } = await render(Image, {
331
+ ...default_props,
332
+ interactive: false,
333
+ value: fake_value,
334
+ buttons: []
335
+ });
336
+
337
+ expect(queryByLabelText("Fullscreen")).toBeNull();
338
+ expect(queryByLabelText("common.download")).toBeNull();
339
+ });
340
+
341
+ test("custom button renders and dispatches custom_button_click", async () => {
342
+ const { listen, getByLabelText } = await render(Image, {
343
+ ...default_props,
344
+ interactive: false,
345
+ value: fake_value,
346
+ buttons: [{ value: "Analyze", id: 7, icon: null }]
347
+ });
348
+
349
+ const custom = listen("custom_button_click");
350
+ const btn = getByLabelText("Analyze");
351
+
352
+ await fireEvent.click(btn);
353
+
354
+ expect(custom).toHaveBeenCalledTimes(1);
355
+ expect(custom).toHaveBeenCalledWith({ id: 7 });
356
+ });
357
+ });
358
+
359
+ describe("Props: buttons (interactive mode)", () => {
360
+ afterEach(() => cleanup());
361
+
362
+ test("clear button appears when image has a value", async () => {
363
+ const { getByLabelText } = await render(Image, {
364
+ ...default_props,
365
+ interactive: true,
366
+ value: fake_value
367
+ });
368
+
369
+ const clearBtn = getByLabelText("Remove Image");
370
+ expect(clearBtn).toBeTruthy();
371
+ });
372
+
373
+ test("clear button is not present when there is no image value", async () => {
374
+ const { queryByLabelText, getByLabelText } = await render(Image, {
375
+ ...default_props,
376
+ interactive: true,
377
+ value: null
378
+ });
379
+
380
+ // Smoke test: component rendered
381
+ expect(getByLabelText("image.drop_to_upload")).toBeTruthy();
382
+ expect(queryByLabelText("Remove Image")).toBeNull();
383
+ });
384
+
385
+ test("clicking clear button removes the image and dispatches clear and input", async () => {
386
+ const { getByLabelText, listen } = await render(Image, {
387
+ ...default_props,
388
+ interactive: true,
389
+ value: fake_value
390
+ });
391
+
392
+ const clear = listen("clear");
393
+ const input = listen("input");
394
+ const clearBtn = getByLabelText("Remove Image");
395
+
396
+ await fireEvent.click(clearBtn);
397
+
398
+ expect(clear).toHaveBeenCalledTimes(1);
399
+ expect(input).toHaveBeenCalledTimes(1);
400
+ });
401
+ });
402
+
403
+ describe("get_data", () => {
404
+ afterEach(() => cleanup());
405
+
406
+ test("get_data returns the current value", async () => {
407
+ const { get_data, set_data } = await render(Image, {
408
+ ...default_props,
409
+ value: null
410
+ });
411
+
412
+ const initial = await get_data();
413
+ expect(initial.value).toBeNull();
414
+
415
+ await set_data({ value: fake_value });
416
+
417
+ const updated = await get_data();
418
+ expect(updated.value).toEqual(fake_value);
419
+ });
420
+ });
421
+
422
+ describe("Selectable", () => {
423
+ afterEach(() => cleanup());
424
+
425
+ test("selectable mode shows crosshair cursor on the image", async () => {
426
+ const { container } = await render(Image, {
427
+ ...default_props,
428
+ interactive: false,
429
+ value: fake_value,
430
+ _selectable: true,
431
+ buttons: []
432
+ });
433
+
434
+ const frame = container.querySelector(".selectable");
435
+ expect(frame).toBeTruthy();
436
+ });
437
+
438
+ test("non-selectable mode does not show crosshair cursor", async () => {
439
+ const { container } = await render(Image, {
440
+ ...default_props,
441
+ interactive: false,
442
+ value: fake_value,
443
+ _selectable: false,
444
+ buttons: []
445
+ });
446
+
447
+ const frame = container.querySelector(".selectable");
448
+ expect(frame).toBeNull();
449
+ });
450
+ });
451
+
452
+ describe("get_coordinates_of_clicked_image", () => {
453
+ function make_mock_event(
454
+ clientX: number,
455
+ clientY: number,
456
+ imgProps: { naturalWidth: number; naturalHeight: number },
457
+ rect: { left: number; top: number; width: number; height: number }
458
+ ): MouseEvent {
459
+ const imgEl = document.createElement("img");
460
+ Object.defineProperty(imgEl, "naturalWidth", {
461
+ value: imgProps.naturalWidth
462
+ });
463
+ Object.defineProperty(imgEl, "naturalHeight", {
464
+ value: imgProps.naturalHeight
465
+ });
466
+ imgEl.getBoundingClientRect = () => ({
467
+ left: rect.left,
468
+ top: rect.top,
469
+ width: rect.width,
470
+ height: rect.height,
471
+ right: rect.left + rect.width,
472
+ bottom: rect.top + rect.height,
473
+ x: rect.left,
474
+ y: rect.top,
475
+ toJSON: () => {}
476
+ });
477
+
478
+ const container = document.createElement("div");
479
+ container.appendChild(imgEl);
480
+
481
+ return {
482
+ currentTarget: container,
483
+ clientX,
484
+ clientY
485
+ } as unknown as MouseEvent;
486
+ }
487
+
488
+ test("returns correct coordinates for a 1:1 scale image", () => {
489
+ const evt = make_mock_event(
490
+ 50,
491
+ 50,
492
+ {
493
+ naturalWidth: 100,
494
+ naturalHeight: 100
495
+ },
496
+ {
497
+ left: 0,
498
+ top: 0,
499
+ width: 100,
500
+ height: 100
501
+ }
502
+ );
503
+
504
+ const result = get_coordinates_of_clicked_image(evt);
505
+ expect(result).toEqual([50, 50]);
506
+ });
507
+
508
+ test("returns correct coordinates when image is scaled down", () => {
509
+ // 200x200 natural, displayed at 100x100, click at (25, 25) in viewport
510
+ const evt = make_mock_event(
511
+ 25,
512
+ 25,
513
+ {
514
+ naturalWidth: 200,
515
+ naturalHeight: 200
516
+ },
517
+ {
518
+ left: 0,
519
+ top: 0,
520
+ width: 100,
521
+ height: 100
522
+ }
523
+ );
524
+
525
+ const result = get_coordinates_of_clicked_image(evt);
526
+ expect(result).toEqual([50, 50]);
527
+ });
528
+
529
+ test("accounts for container offset", () => {
530
+ const evt = make_mock_event(
531
+ 60,
532
+ 70,
533
+ {
534
+ naturalWidth: 100,
535
+ naturalHeight: 100
536
+ },
537
+ {
538
+ left: 10,
539
+ top: 20,
540
+ width: 100,
541
+ height: 100
542
+ }
543
+ );
544
+
545
+ const result = get_coordinates_of_clicked_image(evt);
546
+ expect(result).toEqual([50, 50]);
547
+ });
548
+
549
+ test("returns null when click is outside image bounds", () => {
550
+ // Click at (-5, 50) relative to image → x = -5 which is < 0
551
+ const evt = make_mock_event(
552
+ -5,
553
+ 50,
554
+ {
555
+ naturalWidth: 100,
556
+ naturalHeight: 100
557
+ },
558
+ {
559
+ left: 0,
560
+ top: 0,
561
+ width: 100,
562
+ height: 100
563
+ }
564
+ );
565
+
566
+ const result = get_coordinates_of_clicked_image(evt);
567
+ expect(result).toBeNull();
568
+ });
569
+
570
+ test("returns null when click is beyond the right edge", () => {
571
+ const evt = make_mock_event(
572
+ 105,
573
+ 50,
574
+ {
575
+ naturalWidth: 100,
576
+ naturalHeight: 100
577
+ },
578
+ {
579
+ left: 0,
580
+ top: 0,
581
+ width: 100,
582
+ height: 100
583
+ }
584
+ );
585
+
586
+ const result = get_coordinates_of_clicked_image(evt);
587
+ expect(result).toBeNull();
588
+ });
589
+
590
+ test("handles landscape image with letterboxing (xScale > yScale)", () => {
591
+ // 400x200 natural image displayed in 200x200 container
592
+ // xScale = 400/200 = 2, yScale = 200/200 = 1
593
+ // xScale > yScale, so displayed_height = 200/2 = 100, y_offset = 50
594
+ // Click at (100, 100): x = (100-0)*2 = 200, y = (100-0-50)*2 = 100
595
+ const evt = make_mock_event(
596
+ 100,
597
+ 100,
598
+ {
599
+ naturalWidth: 400,
600
+ naturalHeight: 200
601
+ },
602
+ {
603
+ left: 0,
604
+ top: 0,
605
+ width: 200,
606
+ height: 200
607
+ }
608
+ );
609
+
610
+ const result = get_coordinates_of_clicked_image(evt);
611
+ expect(result).toEqual([200, 100]);
612
+ });
613
+
614
+ test("handles portrait image with pillarboxing (yScale > xScale)", () => {
615
+ // 200x400 natural image displayed in 200x200 container
616
+ // xScale = 200/200 = 1, yScale = 400/200 = 2
617
+ // yScale > xScale, so displayed_width = 200/2 = 100, x_offset = 50
618
+ // Click at (100, 100): x = (100-0-50)*2 = 100, y = (100-0)*2 = 200
619
+ const evt = make_mock_event(
620
+ 100,
621
+ 100,
622
+ {
623
+ naturalWidth: 200,
624
+ naturalHeight: 400
625
+ },
626
+ {
627
+ left: 0,
628
+ top: 0,
629
+ width: 200,
630
+ height: 200
631
+ }
632
+ );
633
+
634
+ const result = get_coordinates_of_clicked_image(evt);
635
+ expect(result).toEqual([100, 200]);
636
+ });
637
+
638
+ test("returns [NaN, NaN] when currentTarget is not an Element", () => {
639
+ const evt = {
640
+ currentTarget: {},
641
+ clientX: 50,
642
+ clientY: 50
643
+ } as unknown as MouseEvent;
644
+
645
+ const result = get_coordinates_of_clicked_image(evt);
646
+ expect(result).toEqual([NaN, NaN]);
647
+ });
648
+ });
649
+
650
+ const upload_props = {
651
+ ...default_props,
652
+ sources: ["upload"] as "upload"[],
653
+ interactive: true,
654
+ value: null,
655
+ root: "https://example.com",
656
+ client: mock_client()
657
+ };
658
+
659
+ describe("Events: upload via file input", () => {
660
+ afterEach(() => cleanup());
661
+
662
+ test("selecting a file triggers upload, change, and input events", async () => {
663
+ const { listen } = await render(Image, upload_props);
664
+
665
+ const upload = listen("upload");
666
+ const change = listen("change");
667
+ const input = listen("input");
668
+
669
+ await upload_file(TEST_JPG);
670
+
671
+ await waitFor(() => {
672
+ expect(upload).toHaveBeenCalledTimes(1);
673
+ });
674
+ expect(input).toHaveBeenCalledTimes(1);
675
+ expect(change).toHaveBeenCalledTimes(1);
676
+ });
677
+
678
+ test("drag and drop a file triggers upload, change, and input events", async () => {
679
+ const { listen } = await render(Image, upload_props);
680
+
681
+ const upload = listen("upload");
682
+ const change = listen("change");
683
+ const input = listen("input");
684
+
685
+ await drop_file(TEST_PNG, "[aria-label='image.drop_to_upload']");
686
+
687
+ await waitFor(() => {
688
+ expect(upload).toHaveBeenCalledTimes(1);
689
+ });
690
+ expect(input).toHaveBeenCalledTimes(1);
691
+ expect(change).toHaveBeenCalledTimes(1);
692
+ });
693
+
694
+ test("upload failure dispatches error event with the message", async () => {
695
+ const failing_upload = vi
696
+ .fn()
697
+ .mockRejectedValue(new Error("File too large"));
698
+ const { listen } = await render(Image, {
699
+ ...upload_props,
700
+ client: {
701
+ upload: failing_upload,
702
+ stream: async () => ({ onmessage: null, close: () => {} })
703
+ }
704
+ });
705
+
706
+ const error = listen("error");
707
+
708
+ await upload_file(TEST_JPG);
709
+
710
+ await waitFor(() => {
711
+ expect(failing_upload).toHaveBeenCalled();
712
+ });
713
+
714
+ await waitFor(() => {
715
+ expect(error).toHaveBeenCalledTimes(1);
716
+ });
717
+ expect(error).toHaveBeenCalledWith("File too large");
718
+ });
719
+ });
package/Index.svelte CHANGED
@@ -70,10 +70,16 @@
70
70
  }
71
71
  };
72
72
 
73
- let old_value = $state(gradio.props.value);
73
+ let old_value = gradio.props.value;
74
+ let mounted = false;
74
75
 
75
76
  $effect(() => {
76
- if (old_value != gradio.props.value) {
77
+ if (!mounted) {
78
+ old_value = gradio.props.value;
79
+ mounted = true;
80
+ return;
81
+ }
82
+ if (old_value !== gradio.props.value) {
77
83
  old_value = gradio.props.value;
78
84
  gradio.dispatch("change");
79
85
  }
@@ -144,7 +150,7 @@
144
150
  on:dragover={handle_drag_event}
145
151
  on:drop={handle_drop}
146
152
  >
147
- {#if gradio.shared.loading_status.type === "output"}
153
+ {#if gradio.shared.loading_status.type === "output" || gradio.shared.loading_status.validation_error}
148
154
  <StatusTracker
149
155
  autoscroll={gradio.shared.autoscroll}
150
156
  i18n={gradio.i18n}
package/dist/Index.svelte CHANGED
@@ -70,10 +70,16 @@
70
70
  }
71
71
  };
72
72
 
73
- let old_value = $state(gradio.props.value);
73
+ let old_value = gradio.props.value;
74
+ let mounted = false;
74
75
 
75
76
  $effect(() => {
76
- if (old_value != gradio.props.value) {
77
+ if (!mounted) {
78
+ old_value = gradio.props.value;
79
+ mounted = true;
80
+ return;
81
+ }
82
+ if (old_value !== gradio.props.value) {
77
83
  old_value = gradio.props.value;
78
84
  gradio.dispatch("change");
79
85
  }
@@ -144,7 +150,7 @@
144
150
  on:dragover={handle_drag_event}
145
151
  on:drop={handle_drop}
146
152
  >
147
- {#if gradio.shared.loading_status.type === "output"}
153
+ {#if gradio.shared.loading_status.type === "output" || gradio.shared.loading_status.validation_error}
148
154
  <StatusTracker
149
155
  autoscroll={gradio.shared.autoscroll}
150
156
  i18n={gradio.i18n}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/image",
3
- "version": "0.26.0",
3
+ "version": "0.26.2",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -10,15 +10,15 @@
10
10
  "cropperjs": "^2.0.1",
11
11
  "lazy-brush": "^2.0.2",
12
12
  "resize-observer-polyfill": "^1.5.1",
13
- "@gradio/client": "^2.1.0",
14
- "@gradio/atoms": "^0.22.2",
15
- "@gradio/statustracker": "^0.13.0",
16
- "@gradio/upload": "^0.17.7",
17
13
  "@gradio/icons": "^0.15.1",
18
- "@gradio/utils": "^0.12.1"
14
+ "@gradio/statustracker": "^0.14.0",
15
+ "@gradio/upload": "^0.17.8",
16
+ "@gradio/client": "^2.2.0",
17
+ "@gradio/utils": "^0.12.2",
18
+ "@gradio/atoms": "^0.23.1"
19
19
  },
20
20
  "devDependencies": {
21
- "@gradio/preview": "^0.16.1"
21
+ "@gradio/preview": "^0.16.2"
22
22
  },
23
23
  "main_changeset": true,
24
24
  "main": "./Index.svelte",