@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 +25 -0
- package/Image.test.ts +719 -0
- package/Index.svelte +9 -3
- package/dist/Index.svelte +9 -3
- package/package.json +7 -7
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 =
|
|
73
|
+
let old_value = gradio.props.value;
|
|
74
|
+
let mounted = false;
|
|
74
75
|
|
|
75
76
|
$effect(() => {
|
|
76
|
-
if (
|
|
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 =
|
|
73
|
+
let old_value = gradio.props.value;
|
|
74
|
+
let mounted = false;
|
|
74
75
|
|
|
75
76
|
$effect(() => {
|
|
76
|
-
if (
|
|
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.
|
|
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/
|
|
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.
|
|
21
|
+
"@gradio/preview": "^0.16.2"
|
|
22
22
|
},
|
|
23
23
|
"main_changeset": true,
|
|
24
24
|
"main": "./Index.svelte",
|