@gradio/downloadbutton 0.4.18 → 0.5.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # @gradio/downloadbutton
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Features
6
+
7
+ - [#13502](https://github.com/gradio-app/gradio/pull/13502) [`429faeb`](https://github.com/gradio-app/gradio/commit/429faeb643fb1afc1722c0f63fafa11603f2c87f) - Ensure every component dispatches a `change` event when its value changes. Thanks @abidlabs!
8
+
9
+ ### Dependency updates
10
+
11
+ - @gradio/client@2.2.2
12
+ - @gradio/button@0.7.0
13
+
14
+ ## 0.4.18
15
+
16
+ ### Dependency updates
17
+
18
+ - @gradio/client@2.2.1
19
+
20
+ ## 0.4.18
21
+
22
+ ### Dependency updates
23
+
24
+ - @gradio/client@2.2.0
25
+
26
+ ## 0.4.18
27
+
28
+ ### Dependency updates
29
+
30
+ - @gradio/utils@0.12.2
31
+
32
+ ## 0.4.18
33
+
34
+ ### Dependency updates
35
+
36
+ - @gradio/utils@0.12.1
37
+ - @gradio/button@0.6.6
38
+
3
39
  ## 0.4.18
4
40
 
5
41
  ### Dependency updates
@@ -0,0 +1,418 @@
1
+ import { test, describe, afterEach, beforeEach, expect, vi } from "vitest";
2
+ import { cleanup, render, fireEvent } from "@self/tootils/render";
3
+ import { run_shared_prop_tests } from "@self/tootils/shared-prop-tests";
4
+
5
+ import DownloadButton from "./Index.svelte";
6
+
7
+ const default_props = {
8
+ value: null,
9
+ variant: "secondary" as const,
10
+ size: "lg" as const,
11
+ icon: null,
12
+ label: "Download",
13
+ interactive: true
14
+ };
15
+
16
+ run_shared_prop_tests({
17
+ component: DownloadButton,
18
+ name: "DownloadButton",
19
+ base_props: { ...default_props },
20
+ has_label: false,
21
+ has_validation_error: false,
22
+ visible_false_hides: true,
23
+ has_block_wrapper: false
24
+ });
25
+
26
+ function spy_anchor_click(): {
27
+ captured: { href: string; download: string }[];
28
+ restore: () => void;
29
+ } {
30
+ const captured: { href: string; download: string }[] = [];
31
+ const spy = vi
32
+ .spyOn(HTMLAnchorElement.prototype, "click")
33
+ .mockImplementation(function (this: HTMLAnchorElement) {
34
+ captured.push({ href: this.href, download: this.download });
35
+ });
36
+ return { captured, restore: () => spy.mockRestore() };
37
+ }
38
+
39
+ describe("Props: label", () => {
40
+ afterEach(() => cleanup());
41
+
42
+ test("renders the label as the button's accessible name", async () => {
43
+ const { getByRole } = await render(DownloadButton, {
44
+ ...default_props,
45
+ label: "download CSV"
46
+ });
47
+
48
+ expect(getByRole("button", { name: "download CSV" })).toBeVisible();
49
+ });
50
+
51
+ test("null label renders an empty button", async () => {
52
+ const { getByRole } = await render(DownloadButton, {
53
+ ...default_props,
54
+ label: null
55
+ });
56
+
57
+ expect(getByRole("button")).toBeVisible();
58
+ });
59
+ });
60
+
61
+ describe("Props: icon", () => {
62
+ afterEach(() => cleanup());
63
+
64
+ test("renders icon image when icon prop is set", async () => {
65
+ const { getByRole } = await render(DownloadButton, {
66
+ ...default_props,
67
+ icon: {
68
+ url: "https://gradio.app/icon.svg",
69
+ path: "icon.svg",
70
+ orig_name: "icon.svg"
71
+ }
72
+ });
73
+
74
+ const img = getByRole("img");
75
+ expect(img).toHaveAttribute("src", "https://gradio.app/icon.svg");
76
+ });
77
+
78
+ test("does not render icon image when icon prop is null", async () => {
79
+ const { queryByRole } = await render(DownloadButton, {
80
+ ...default_props,
81
+ icon: null
82
+ });
83
+
84
+ expect(queryByRole("img")).not.toBeInTheDocument();
85
+ });
86
+ });
87
+
88
+ describe("Props: value", () => {
89
+ let anchor_spy: ReturnType<typeof spy_anchor_click>;
90
+
91
+ beforeEach(() => {
92
+ anchor_spy = spy_anchor_click();
93
+ });
94
+
95
+ afterEach(() => {
96
+ anchor_spy.restore();
97
+ cleanup();
98
+ });
99
+
100
+ test("clicking triggers a synthetic anchor click with the file URL", async () => {
101
+ const { getByRole } = await render(DownloadButton, {
102
+ ...default_props,
103
+ value: {
104
+ url: "https://gradio.app/files/report.pdf",
105
+ orig_name: "report.pdf",
106
+ path: "report.pdf"
107
+ } as any
108
+ });
109
+
110
+ await fireEvent.click(getByRole("button", { name: "Download" }));
111
+
112
+ expect(anchor_spy.captured).toHaveLength(1);
113
+ expect(anchor_spy.captured[0].href).toBe(
114
+ "https://gradio.app/files/report.pdf"
115
+ );
116
+ });
117
+
118
+ test("uses orig_name as the download filename when provided", async () => {
119
+ const { getByRole } = await render(DownloadButton, {
120
+ ...default_props,
121
+ value: {
122
+ url: "https://gradio.app/abc123",
123
+ orig_name: "weekly-downloads.pdf",
124
+ path: "abc123"
125
+ } as any
126
+ });
127
+
128
+ await fireEvent.click(getByRole("button", { name: "Download" }));
129
+
130
+ expect(anchor_spy.captured[0].download).toBe("weekly-downloads.pdf");
131
+ });
132
+
133
+ test("derives filename from URL when orig_name is missing", async () => {
134
+ const { getByRole } = await render(DownloadButton, {
135
+ ...default_props,
136
+ value: {
137
+ url: "https://gradio.app/files/data.csv",
138
+ orig_name: "",
139
+ path: "files/data.csv"
140
+ } as any
141
+ });
142
+
143
+ await fireEvent.click(getByRole("button", { name: "Download" }));
144
+
145
+ expect(anchor_spy.captured[0].download).toBe("data.csv");
146
+ });
147
+
148
+ test("strips query string from URL-derived filename", async () => {
149
+ const { getByRole } = await render(DownloadButton, {
150
+ ...default_props,
151
+ value: {
152
+ url: "https://gradio.app/files/data.csv?token=abc&v=2",
153
+ orig_name: "",
154
+ path: "files/data.csv"
155
+ } as any
156
+ });
157
+
158
+ await fireEvent.click(getByRole("button", { name: "Download" }));
159
+
160
+ expect(anchor_spy.captured[0].download).toBe("data.csv");
161
+ });
162
+
163
+ test("strips hash fragment from URL-derived filename", async () => {
164
+ const { getByRole } = await render(DownloadButton, {
165
+ ...default_props,
166
+ value: {
167
+ url: "https://gradio.app/files/notes.md#section-2",
168
+ orig_name: "",
169
+ path: "files/notes.md"
170
+ } as any
171
+ });
172
+
173
+ await fireEvent.click(getByRole("button", { name: "Download" }));
174
+
175
+ expect(anchor_spy.captured[0].download).toBe("notes.md");
176
+ });
177
+
178
+ test("does not trigger download when value is null", async () => {
179
+ const { getByRole } = await render(DownloadButton, {
180
+ ...default_props,
181
+ value: null
182
+ });
183
+
184
+ await fireEvent.click(getByRole("button", { name: "Download" }));
185
+
186
+ expect(anchor_spy.captured).toHaveLength(0);
187
+ });
188
+
189
+ test("does not trigger download when value.url is missing", async () => {
190
+ const { getByRole } = await render(DownloadButton, {
191
+ ...default_props,
192
+ value: { url: "", orig_name: "file.txt", path: "file.txt" } as any
193
+ });
194
+
195
+ await fireEvent.click(getByRole("button", { name: "Download" }));
196
+
197
+ expect(anchor_spy.captured).toHaveLength(0);
198
+ });
199
+ });
200
+
201
+ describe("Props: disabled", () => {
202
+ afterEach(() => cleanup());
203
+
204
+ test("interactive=false renders the button as disabled", async () => {
205
+ const { getByRole } = await render(DownloadButton, {
206
+ ...default_props,
207
+ interactive: false,
208
+ value: {
209
+ url: "https://gradio.app/files/report.pdf",
210
+ orig_name: "report.pdf",
211
+ path: "report.pdf"
212
+ } as any
213
+ });
214
+
215
+ expect(getByRole("button", { name: "Download" })).toBeDisabled();
216
+ });
217
+
218
+ test("interactive=true renders the button as enabled", async () => {
219
+ const { getByRole } = await render(DownloadButton, {
220
+ ...default_props,
221
+ interactive: true
222
+ });
223
+
224
+ expect(getByRole("button", { name: "Download" })).toBeEnabled();
225
+ });
226
+ });
227
+
228
+ describe("get_data / set_data", () => {
229
+ let anchor_spy: ReturnType<typeof spy_anchor_click>;
230
+
231
+ beforeEach(() => {
232
+ anchor_spy = spy_anchor_click();
233
+ });
234
+
235
+ afterEach(() => {
236
+ anchor_spy.restore();
237
+ cleanup();
238
+ });
239
+
240
+ test("get_data returns the current value", async () => {
241
+ const file_value = {
242
+ url: "https://gradio.app/files/report.pdf",
243
+ orig_name: "report.pdf",
244
+ path: "report.pdf"
245
+ };
246
+ const { get_data } = await render(DownloadButton, {
247
+ ...default_props,
248
+ value: file_value as any
249
+ });
250
+
251
+ expect((await get_data()).value).toEqual(file_value);
252
+ });
253
+
254
+ test("get_data returns null when no value is set", async () => {
255
+ const { get_data } = await render(DownloadButton, {
256
+ ...default_props,
257
+ value: null
258
+ });
259
+
260
+ expect((await get_data()).value).toBeNull();
261
+ });
262
+
263
+ test("set_data updates the value used for download", async () => {
264
+ const { getByRole, set_data } = await render(DownloadButton, {
265
+ ...default_props,
266
+ value: null
267
+ });
268
+
269
+ await set_data({
270
+ value: {
271
+ url: "https://gradio.app/files/updated.pdf",
272
+ orig_name: "updated.pdf",
273
+ path: "updated.pdf"
274
+ }
275
+ });
276
+
277
+ await fireEvent.click(getByRole("button", { name: "Download" }));
278
+
279
+ expect(anchor_spy.captured).toHaveLength(1);
280
+ expect(anchor_spy.captured[0].href).toBe(
281
+ "https://gradio.app/files/updated.pdf"
282
+ );
283
+ expect(anchor_spy.captured[0].download).toBe("updated.pdf");
284
+ });
285
+
286
+ test("set_data then get_data round-trips the value", async () => {
287
+ const { get_data, set_data } = await render(DownloadButton, {
288
+ ...default_props,
289
+ value: null
290
+ });
291
+
292
+ const new_value = {
293
+ url: "https://gradio.app/files/data.csv",
294
+ orig_name: "data.csv",
295
+ path: "data.csv"
296
+ };
297
+ await set_data({ value: new_value });
298
+
299
+ expect((await get_data()).value).toEqual(new_value);
300
+ });
301
+ });
302
+
303
+ describe("Events", () => {
304
+ let anchor_spy: ReturnType<typeof spy_anchor_click>;
305
+
306
+ beforeEach(() => {
307
+ anchor_spy = spy_anchor_click();
308
+ });
309
+
310
+ afterEach(() => {
311
+ anchor_spy.restore();
312
+ cleanup();
313
+ });
314
+
315
+ test("does not dispatch spurious click event on mount", async () => {
316
+ const { listen } = await render(DownloadButton, {
317
+ ...default_props,
318
+ value: {
319
+ url: "https://gradio.app/file.zip",
320
+ orig_name: "file.zip",
321
+ path: "file.zip"
322
+ } as any
323
+ });
324
+
325
+ const click = listen("click", { retrospective: true });
326
+ expect(click).not.toHaveBeenCalled();
327
+ });
328
+
329
+ test("dispatches click event with a downloadable value", async () => {
330
+ const { getByRole, listen } = await render(DownloadButton, {
331
+ ...default_props,
332
+ value: {
333
+ url: "https://gradio.app/file.zip",
334
+ orig_name: "file.zip",
335
+ path: "file.zip"
336
+ } as any
337
+ });
338
+
339
+ const click = listen("click");
340
+ await fireEvent.click(getByRole("button", { name: "Download" }));
341
+
342
+ expect(click).toHaveBeenCalledTimes(1);
343
+ });
344
+
345
+ test("dispatches click event even when value is null", async () => {
346
+ const { getByRole, listen } = await render(DownloadButton, {
347
+ ...default_props,
348
+ value: null
349
+ });
350
+
351
+ const click = listen("click");
352
+ await fireEvent.click(getByRole("button", { name: "Download" }));
353
+
354
+ expect(click).toHaveBeenCalledTimes(1);
355
+ });
356
+ });
357
+
358
+ describe("Edge cases", () => {
359
+ let anchor_spy: ReturnType<typeof spy_anchor_click>;
360
+
361
+ beforeEach(() => {
362
+ anchor_spy = spy_anchor_click();
363
+ });
364
+
365
+ afterEach(() => {
366
+ anchor_spy.restore();
367
+ cleanup();
368
+ });
369
+
370
+ test("falls back to 'file' when orig_name is empty and URL ends with a trailing slash", async () => {
371
+ const { getByRole } = await render(DownloadButton, {
372
+ ...default_props,
373
+ value: {
374
+ url: "https://gradio.app/files/",
375
+ orig_name: "",
376
+ path: "files/"
377
+ } as any
378
+ });
379
+
380
+ await fireEvent.click(getByRole("button", { name: "Download" }));
381
+
382
+ expect(anchor_spy.captured).toHaveLength(1);
383
+ expect(anchor_spy.captured[0].download).toBe("file");
384
+ });
385
+
386
+ test("falls back to 'file' when URL has no path segments after domain", async () => {
387
+ const { getByRole } = await render(DownloadButton, {
388
+ ...default_props,
389
+ value: {
390
+ url: "https://gradio.app/",
391
+ orig_name: "",
392
+ path: ""
393
+ } as any
394
+ });
395
+
396
+ await fireEvent.click(getByRole("button", { name: "Download" }));
397
+
398
+ expect(anchor_spy.captured).toHaveLength(1);
399
+ expect(anchor_spy.captured[0].download).toBe("file");
400
+ });
401
+
402
+ test("does not leak anchor elements into document.body after download", async () => {
403
+ const anchors_before = document.body.querySelectorAll("a").length;
404
+
405
+ const { getByRole } = await render(DownloadButton, {
406
+ ...default_props,
407
+ value: {
408
+ url: "https://gradio.app/files/report.pdf",
409
+ orig_name: "report.pdf",
410
+ path: "report.pdf"
411
+ } as any
412
+ });
413
+
414
+ await fireEvent.click(getByRole("button", { name: "Download" }));
415
+
416
+ expect(document.body.querySelectorAll("a").length).toBe(anchors_before);
417
+ });
418
+ });
package/Index.svelte CHANGED
@@ -9,6 +9,7 @@
9
9
 
10
10
  const props = $props();
11
11
  const gradio = new Gradio<DownloadButtonEvents, DownloadButtonProps>(props);
12
+ gradio.watch_for_change();
12
13
  </script>
13
14
 
14
15
  <DownloadButton
package/dist/Index.svelte CHANGED
@@ -9,6 +9,7 @@
9
9
 
10
10
  const props = $props();
11
11
  const gradio = new Gradio<DownloadButtonEvents, DownloadButtonProps>(props);
12
+ gradio.watch_for_change();
12
13
  </script>
13
14
 
14
15
  <DownloadButton
package/dist/types.d.ts CHANGED
@@ -6,5 +6,6 @@ export interface DownloadButtonProps {
6
6
  icon: FileData | null;
7
7
  }
8
8
  export interface DownloadButtonEvents {
9
+ change: never;
9
10
  click: never;
10
11
  }
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@gradio/downloadbutton",
3
- "version": "0.4.18",
3
+ "version": "0.5.0",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
7
7
  "license": "ISC",
8
8
  "private": false,
9
9
  "dependencies": {
10
- "@gradio/button": "^0.6.5",
11
- "@gradio/utils": "^0.12.0",
12
- "@gradio/client": "^2.1.0"
10
+ "@gradio/button": "^0.7.0",
11
+ "@gradio/client": "^2.2.2",
12
+ "@gradio/utils": "^0.12.2"
13
13
  },
14
14
  "devDependencies": {
15
- "@gradio/preview": "^0.16.0"
15
+ "@gradio/preview": "^0.16.2"
16
16
  },
17
17
  "main": "./Index.svelte",
18
18
  "main_changeset": true,
package/types.ts CHANGED
@@ -8,5 +8,6 @@ export interface DownloadButtonProps {
8
8
  }
9
9
 
10
10
  export interface DownloadButtonEvents {
11
+ change: never;
11
12
  click: never;
12
13
  }