@gradio/video 0.20.4 → 0.20.6

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,27 @@
1
1
  # @gradio/video
2
2
 
3
+ ## 0.20.6
4
+
5
+ ### Features
6
+
7
+ - [#13167](https://github.com/gradio-app/gradio/pull/13167) [`a4e1c92`](https://github.com/gradio-app/gradio/commit/a4e1c92c11e05bee332ff69e19b533fbd9abc840) - Audio and Video unit tests. Thanks @freddyaboulton!
8
+
9
+ ### Dependency updates
10
+
11
+ - @gradio/utils@0.12.2
12
+ - @gradio/atoms@0.23.0
13
+ - @gradio/statustracker@0.13.1
14
+ - @gradio/upload@0.17.8
15
+ - @gradio/image@0.26.1
16
+
17
+ ## 0.20.5
18
+
19
+ ### Dependency updates
20
+
21
+ - @gradio/utils@0.12.1
22
+ - @gradio/statustracker@0.13.0
23
+ - @gradio/image@0.26.0
24
+
3
25
  ## 0.20.4
4
26
 
5
27
  ### Dependency updates
package/Index.svelte CHANGED
@@ -27,7 +27,8 @@
27
27
  }
28
28
 
29
29
  const gradio = new VideoGradio(props);
30
- let old_value = $state(gradio.props.value);
30
+ let old_value = gradio.props.value;
31
+ let mounted = false;
31
32
 
32
33
  let uploading = $state(false);
33
34
  let dragging = $state(false);
@@ -37,6 +38,11 @@
37
38
  let initial_value: FileData | null = gradio.props.value;
38
39
 
39
40
  $effect(() => {
41
+ if (!mounted) {
42
+ old_value = gradio.props.value;
43
+ mounted = true;
44
+ return;
45
+ }
40
46
  if (old_value != gradio.props.value) {
41
47
  old_value = gradio.props.value;
42
48
  gradio.dispatch("change");
package/Video.test.ts CHANGED
@@ -5,11 +5,19 @@ import {
5
5
  afterEach,
6
6
  vi,
7
7
  beforeAll,
8
- beforeEach,
9
8
  expect
10
9
  } from "vitest";
11
- import { spyOn } from "tinyspy";
12
- import { cleanup, render } from "@self/tootils";
10
+ import {
11
+ cleanup,
12
+ render,
13
+ fireEvent,
14
+ waitFor,
15
+ upload_file,
16
+ drop_file,
17
+ mock_client,
18
+ TEST_MP4
19
+ } from "@self/tootils/render";
20
+ import { run_shared_prop_tests } from "@self/tootils/shared-prop-tests";
13
21
  import { setupi18n } from "../core/src/i18n";
14
22
 
15
23
  vi.mock("@ffmpeg/ffmpeg", () => ({
@@ -29,316 +37,501 @@ vi.mock("@ffmpeg/util", () => ({
29
37
  }));
30
38
 
31
39
  import Video from "./Index.svelte";
32
-
33
40
  import type { LoadingStatus } from "@gradio/statustracker";
34
41
 
35
- const loading_status = {
42
+ const loading_status: LoadingStatus = {
36
43
  eta: 0,
37
44
  queue_position: 1,
38
45
  queue_size: 1,
39
- status: "complete" as LoadingStatus["status"],
46
+ status: "complete",
40
47
  scroll_to_output: false,
41
48
  visible: true,
42
49
  fn_index: 0,
43
- show_progress: "full" as LoadingStatus["show_progress"]
50
+ show_progress: "full",
51
+ type: "input" as const,
52
+ stream_state: "closed" as const
53
+ };
54
+
55
+ const fake_value = {
56
+ path: "video_sample.mp4",
57
+ url: "https://example.com/video_sample.mp4",
58
+ orig_name: "video_sample.mp4",
59
+ size: 261179,
60
+ mime_type: "video/mp4",
61
+ is_stream: false
62
+ };
63
+
64
+ const default_props = {
65
+ loading_status,
66
+ label: "Video",
67
+ show_label: true,
68
+ value: null as any,
69
+ sources: ["upload", "webcam"] as ("upload" | "webcam")[],
70
+ interactive: true,
71
+ streaming: false,
72
+ pending: false,
73
+ autoplay: false,
74
+ loop: false,
75
+ webcam_options: { mirror: false, constraints: {} },
76
+ buttons: [] as (string | { value: string; id: number; icon: null })[]
44
77
  };
45
78
 
79
+ beforeAll(() => {
80
+ window.HTMLMediaElement.prototype.play = vi.fn();
81
+ window.HTMLMediaElement.prototype.pause = vi.fn();
82
+ });
83
+
84
+ run_shared_prop_tests({
85
+ component: Video,
86
+ name: "Video",
87
+ base_props: {
88
+ ...default_props
89
+ },
90
+ has_label: false,
91
+ has_validation_error: false
92
+ });
93
+
46
94
  describe("Video", () => {
47
- beforeAll(() => {
48
- window.HTMLMediaElement.prototype.play = vi.fn();
49
- window.HTMLMediaElement.prototype.pause = vi.fn();
95
+ setupi18n();
96
+ afterEach(() => cleanup());
97
+
98
+ test("renders upload area when value is null", async () => {
99
+ const { getByLabelText } = await render(Video, {
100
+ ...default_props,
101
+ sources: ["upload"],
102
+ value: null
103
+ });
104
+
105
+ expect(getByLabelText("video.drop_to_upload")).toBeVisible();
106
+ });
107
+
108
+ test("renders video player when value is set", async () => {
109
+ const { getByTestId } = await render(Video, {
110
+ ...default_props,
111
+ label: "Test Video",
112
+ value: fake_value
113
+ });
114
+
115
+ const vid = getByTestId("Test Video-player") as HTMLVideoElement;
116
+ expect(vid).toBeTruthy();
117
+ expect(vid.src).toContain("video_sample.mp4");
50
118
  });
51
- beforeEach(setupi18n);
119
+ });
120
+
121
+ describe("Props: sources", () => {
122
+ setupi18n();
52
123
  afterEach(() => cleanup());
53
124
 
54
- test("renders provided value and label", async () => {
55
- const { getByTestId, queryAllByText } = await render(Video, {
56
- show_label: true,
57
- loading_status,
58
- value: {
59
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
60
- url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
61
- },
62
- label: "Test Label",
63
- root: "foo",
64
- proxy_url: null,
65
- streaming: false,
66
- pending: false,
67
- name: "bar",
125
+ test("multiple sources renders source selection buttons", async () => {
126
+ const { getByTestId } = await render(Video, {
127
+ ...default_props,
128
+ sources: ["upload", "webcam"]
129
+ });
130
+
131
+ expect(getByTestId("source-select")).toBeTruthy();
132
+ });
133
+
134
+ test("single upload source does not render source selection", async () => {
135
+ const { queryByTestId } = await render(Video, {
136
+ ...default_props,
137
+ sources: ["upload"]
138
+ });
139
+
140
+ expect(queryByTestId("source-select")).toBeNull();
141
+ });
142
+
143
+ test("upload and webcam sources render corresponding buttons", async () => {
144
+ const { getByLabelText } = await render(Video, {
145
+ ...default_props,
146
+ sources: ["upload", "webcam"]
147
+ });
148
+
149
+ expect(getByLabelText("Upload file")).toBeTruthy();
150
+ expect(getByLabelText("Capture from camera")).toBeTruthy();
151
+ });
152
+ });
153
+
154
+ describe("Props: interactive", () => {
155
+ setupi18n();
156
+ afterEach(() => cleanup());
157
+
158
+ test("interactive=true shows upload area when value is null", async () => {
159
+ const { getByLabelText } = await render(Video, {
160
+ ...default_props,
161
+ interactive: true,
68
162
  sources: ["upload"],
163
+ value: null
164
+ });
165
+
166
+ expect(getByLabelText("video.drop_to_upload")).toBeTruthy();
167
+ });
168
+
169
+ test("interactive=false renders the video without upload controls", async () => {
170
+ const { queryByLabelText } = await render(Video, {
171
+ ...default_props,
172
+ interactive: false,
173
+ value: fake_value
174
+ });
175
+
176
+ expect(queryByLabelText("video.drop_to_upload")).toBeNull();
177
+ expect(queryByLabelText("Upload file")).toBeNull();
178
+ });
179
+
180
+ test("interactive=false with null value does not show upload area", async () => {
181
+ const { queryByLabelText } = await render(Video, {
182
+ ...default_props,
183
+ interactive: false,
184
+ value: null
185
+ });
186
+
187
+ expect(queryByLabelText("video.drop_to_upload")).toBeNull();
188
+ });
189
+ });
190
+
191
+ describe("Events: change", () => {
192
+ setupi18n();
193
+ afterEach(() => cleanup());
194
+
195
+ test("setting value triggers change event", async () => {
196
+ const { listen, set_data } = await render(Video, {
197
+ ...default_props,
198
+ value: null
199
+ });
200
+
201
+ const change = listen("change");
202
+
203
+ await set_data({ value: fake_value });
204
+
205
+ expect(change).toHaveBeenCalledTimes(1);
206
+ });
207
+
208
+ test("change event is not triggered on mount with a default value", async () => {
209
+ const { listen } = await render(Video, {
210
+ ...default_props,
211
+ value: fake_value
212
+ });
213
+
214
+ const change = listen("change", { retrospective: true });
215
+
216
+ expect(change).not.toHaveBeenCalled();
217
+ });
218
+
219
+ test("changing value multiple times triggers change each time", async () => {
220
+ const { listen, set_data } = await render(Video, {
221
+ ...default_props,
222
+ value: null
223
+ });
224
+
225
+ const change = listen("change");
226
+
227
+ const value_a = { ...fake_value, url: "https://example.com/a.mp4" };
228
+ const value_b = { ...fake_value, url: "https://example.com/b.mp4" };
229
+
230
+ await set_data({ value: value_a });
231
+ await set_data({ value: value_b });
232
+
233
+ expect(change).toHaveBeenCalledTimes(2);
234
+ });
235
+
236
+ test("setting value to null after a value triggers change", async () => {
237
+ const { listen, set_data } = await render(Video, {
238
+ ...default_props,
239
+ value: fake_value
240
+ });
241
+
242
+ const change = listen("change");
243
+
244
+ await set_data({ value: null });
245
+
246
+ expect(change).toHaveBeenCalledTimes(1);
247
+ });
248
+ });
249
+
250
+ describe("Props: buttons (static mode)", () => {
251
+ setupi18n();
252
+ afterEach(() => cleanup());
253
+
254
+ test("buttons with download shows download button", async () => {
255
+ const { getAllByTestId } = await render(Video, {
256
+ ...default_props,
257
+ interactive: false,
258
+ value: fake_value,
259
+ buttons: ["download"]
260
+ });
261
+
262
+ const downloadDiv = getAllByTestId("download-div")[0];
263
+ expect(downloadDiv).toBeTruthy();
264
+ const link = downloadDiv.querySelector("a");
265
+ expect(link?.getAttribute("href")).toContain("video_sample.mp4");
266
+ });
267
+
268
+ test("custom button renders and dispatches custom_button_click", async () => {
269
+ const { listen, getByLabelText } = await render(Video, {
270
+ ...default_props,
271
+ interactive: false,
272
+ value: fake_value,
273
+ buttons: [{ value: "Analyze", id: 5, icon: null }]
274
+ });
275
+
276
+ const custom = listen("custom_button_click");
277
+ const btn = getByLabelText("Analyze");
278
+
279
+ await fireEvent.click(btn);
280
+
281
+ expect(custom).toHaveBeenCalledTimes(1);
282
+ expect(custom).toHaveBeenCalledWith({ id: 5 });
283
+ });
284
+ });
285
+
286
+ describe("Props: buttons (interactive mode)", () => {
287
+ setupi18n();
288
+ afterEach(() => cleanup());
289
+
290
+ test("clear button appears when video has a value", async () => {
291
+ const { getByLabelText } = await render(Video, {
292
+ ...default_props,
69
293
  interactive: true,
70
- webcam_options: {
71
- mirror: true,
72
- constraints: null
73
- }
294
+ value: fake_value
74
295
  });
75
- let vid = getByTestId("Test Label-player") as HTMLVideoElement;
76
- assert.equal(
77
- vid.src,
78
- "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
79
- );
80
- assert.equal(queryAllByText("Test Label").length, 1);
296
+
297
+ const clearBtn = getByLabelText("common.clear");
298
+ expect(clearBtn).toBeTruthy();
81
299
  });
82
300
 
83
- test("hides label", async () => {
84
- const { queryAllByText } = await render(Video, {
85
- show_label: false,
86
- loading_status,
87
- value: {
88
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
89
- url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
90
- },
91
- label: "Video Component",
92
- root: "foo",
93
- proxy_url: null,
94
- streaming: false,
95
- pending: false,
96
- name: "bar",
97
- sources: ["upload"],
301
+ test("clicking clear button removes the video and dispatches clear and input", async () => {
302
+ const { getByLabelText, listen } = await render(Video, {
303
+ ...default_props,
98
304
  interactive: true,
99
- webcam_options: {
100
- mirror: true,
101
- constraints: null
102
- }
305
+ value: fake_value
103
306
  });
104
- assert.equal(queryAllByText("Video Component").length, 1);
307
+
308
+ const clear = listen("clear");
309
+ const input = listen("input");
310
+ const clearBtn = getByLabelText("common.clear");
311
+
312
+ await fireEvent.click(clearBtn);
313
+
314
+ expect(clear).toHaveBeenCalledTimes(1);
315
+ expect(input).toHaveBeenCalledTimes(1);
105
316
  });
317
+ });
106
318
 
107
- test("static Video sets value", async () => {
108
- const { getByTestId } = await render(Video, {
109
- show_label: true,
110
- loading_status,
111
- value: {
112
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
113
- url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
114
- },
115
- root: "foo",
116
- proxy_url: null,
117
- streaming: false,
118
- pending: false,
119
- name: "bar",
120
- sources: ["upload"],
121
- mode: "static",
122
- webcam_options: {
123
- mirror: true,
124
- constraints: null
125
- }
319
+ describe("get_data", () => {
320
+ setupi18n();
321
+ afterEach(() => cleanup());
322
+
323
+ test("get_data returns the current value", async () => {
324
+ const { get_data, set_data } = await render(Video, {
325
+ ...default_props,
326
+ value: null
126
327
  });
127
- let vid = getByTestId("test-player") as HTMLVideoElement;
128
- assert.equal(
129
- vid.src,
130
- "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
131
- );
328
+
329
+ const initial = await get_data();
330
+ expect(initial.value).toBeNull();
331
+
332
+ await set_data({ value: fake_value });
333
+
334
+ const updated = await get_data();
335
+ expect(updated.value).toEqual(fake_value);
132
336
  });
337
+ });
133
338
 
134
- test("when autoplay is true `media.play` should be called in static mode", async () => {
339
+ describe("Autoplay", () => {
340
+ setupi18n();
341
+ afterEach(() => cleanup());
342
+
343
+ test("autoplay calls media.play in static mode", async () => {
135
344
  const { getByTestId } = await render(Video, {
136
- show_label: true,
137
- loading_status,
345
+ ...default_props,
138
346
  interactive: false,
139
- value: {
140
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
141
- url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
142
- },
143
- root: "foo",
144
- proxy_url: null,
145
- streaming: false,
146
- pending: false,
147
- sources: ["upload"],
148
- autoplay: true,
149
- webcam_options: {
150
- mirror: true,
151
- constraints: null
152
- }
347
+ value: fake_value,
348
+ autoplay: true
153
349
  });
154
- const startButton = getByTestId("test-player") as HTMLVideoElement;
155
- const fn = spyOn(startButton, "play");
156
- startButton.dispatchEvent(new Event("loadeddata"));
157
- assert.equal(fn.callCount, 1);
350
+
351
+ const video = getByTestId("Video-player") as HTMLVideoElement;
352
+ const fn = vi.spyOn(video, "play").mockResolvedValue(undefined);
353
+ video.dispatchEvent(new Event("loadeddata"));
354
+ expect(fn).toHaveBeenCalledTimes(1);
158
355
  });
159
356
 
160
- test("when autoplay is true `media.play` should be called in dynamic mode", async () => {
357
+ test("autoplay calls media.play in interactive mode", async () => {
161
358
  const { getByTestId } = await render(Video, {
162
- show_label: true,
163
- loading_status,
164
- value: {
165
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
166
- url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
167
- },
168
- root: "foo",
169
- proxy_url: null,
170
- streaming: false,
171
- pending: false,
172
- sources: ["upload"],
173
- autoplay: true,
174
- webcam_options: {
175
- mirror: true,
176
- constraints: null
177
- }
359
+ ...default_props,
360
+ interactive: true,
361
+ value: fake_value,
362
+ autoplay: true
178
363
  });
179
- const startButton = getByTestId("test-player") as HTMLVideoElement;
180
- const fn = spyOn(startButton, "play");
181
- startButton.dispatchEvent(new Event("loadeddata"));
182
- assert.equal(fn.callCount, 1);
364
+
365
+ const video = getByTestId("Video-player") as HTMLVideoElement;
366
+ const fn = vi.spyOn(video, "play").mockResolvedValue(undefined);
367
+ video.dispatchEvent(new Event("loadeddata"));
368
+ expect(fn).toHaveBeenCalled();
183
369
  });
370
+ });
184
371
 
185
- test("when autoplay is true `media.play` should be called in static mode when the Video data is updated", async () => {
186
- const { getByTestId, unmount } = await render(Video, {
187
- show_label: true,
188
- loading_status,
189
- interactive: false,
190
- value: {
191
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
192
- url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
193
- },
194
- root: "foo",
195
- proxy_url: null,
196
- streaming: false,
197
- pending: false,
198
- sources: ["upload"],
199
- autoplay: true,
200
- webcam_options: {
201
- mirror: true,
202
- constraints: null
203
- }
372
+ describe("Player controls", () => {
373
+ setupi18n();
374
+ afterEach(() => cleanup());
375
+
376
+ test("play/pause button is rendered when video has a value", async () => {
377
+ const { getByLabelText } = await render(Video, {
378
+ ...default_props,
379
+ interactive: true,
380
+ value: fake_value
204
381
  });
205
- let startButton = getByTestId("test-player") as HTMLVideoElement;
206
- const fn = spyOn(startButton, "play");
207
- startButton.dispatchEvent(new Event("loadeddata"));
208
- assert.equal(fn.callCount, 1);
209
- unmount();
210
382
 
211
- const result = await render(Video, {
212
- show_label: true,
213
- loading_status,
214
- interactive: false,
215
- value: {
216
- path: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
217
- url: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav"
218
- },
219
- root: "foo",
220
- proxy_url: null,
221
- streaming: false,
222
- pending: false,
223
- sources: ["upload"],
224
- autoplay: true,
225
- webcam_options: {
226
- mirror: true,
227
- constraints: null
228
- }
383
+ expect(getByLabelText("play-pause-replay-button")).toBeTruthy();
384
+ });
385
+
386
+ test("volume button is rendered", async () => {
387
+ const { getByLabelText } = await render(Video, {
388
+ ...default_props,
389
+ interactive: true,
390
+ value: fake_value
229
391
  });
230
- startButton = result.getByTestId("test-player") as HTMLVideoElement;
231
- const fn2 = spyOn(startButton, "play");
232
- startButton.dispatchEvent(new Event("loadeddata"));
233
- assert.equal(fn2.callCount, 1);
392
+
393
+ expect(getByLabelText("Adjust volume")).toBeTruthy();
234
394
  });
235
395
 
236
- test("when autoplay is true `media.play` should be called in dynamic mode when the Video data is updated", async () => {
237
- const { getByTestId, unmount } = await render(Video, {
238
- show_label: true,
239
- loading_status,
396
+ test("fullscreen button is rendered", async () => {
397
+ const { getByLabelText } = await render(Video, {
398
+ ...default_props,
240
399
  interactive: true,
241
- value: {
242
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
243
- url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
244
- },
245
- root: "foo",
246
- proxy_url: null,
247
- streaming: false,
248
- pending: false,
249
- sources: ["upload"],
250
- autoplay: true,
251
- webcam_options: {
252
- mirror: true,
253
- constraints: null
254
- }
400
+ value: fake_value
255
401
  });
256
- let startButton = getByTestId("test-player") as HTMLVideoElement;
257
- const fn = spyOn(startButton, "play");
258
- startButton.dispatchEvent(new Event("loadeddata"));
259
- assert.equal(fn.callCount, 1);
260
- unmount();
261
402
 
262
- const result = await render(Video, {
263
- show_label: true,
264
- loading_status,
403
+ expect(getByLabelText("full-screen")).toBeTruthy();
404
+ });
405
+
406
+ test("trim button is rendered in interactive mode", async () => {
407
+ const { getByLabelText } = await render(Video, {
408
+ ...default_props,
265
409
  interactive: true,
266
- value: {
267
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
268
- url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
269
- },
270
- root: "foo",
271
- proxy_url: null,
272
- streaming: false,
273
- pending: false,
274
- sources: ["upload"],
275
- autoplay: true,
276
- webcam_options: {
277
- mirror: true,
278
- constraints: null
279
- }
410
+ value: fake_value
280
411
  });
281
- startButton = result.getByTestId("test-player") as HTMLVideoElement;
282
- const fnResult = spyOn(startButton, "play");
283
- startButton.dispatchEvent(new Event("loadeddata"));
284
- assert.equal(fnResult.callCount, 1);
412
+
413
+ expect(getByLabelText("Trim video to selection")).toBeTruthy();
285
414
  });
286
- test("renders video and download button", async () => {
287
- const data = {
288
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
289
- url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
290
- };
291
- const results = await render(Video, {
415
+
416
+ test("trim button is not rendered in static mode", async () => {
417
+ const { queryByLabelText } = await render(Video, {
418
+ ...default_props,
292
419
  interactive: false,
293
- label: "video",
294
- show_label: true,
295
- value: data,
296
- root: "https://localhost:8000",
297
- webcam_options: {
298
- mirror: true,
299
- constraints: null
300
- }
420
+ value: fake_value
301
421
  });
302
422
 
303
- const downloadButton = results.getAllByTestId("download-div")[0];
304
- expect(
305
- downloadButton.getElementsByTagName("a")[0].getAttribute("href")
306
- ).toBe(data.path);
307
- expect(
308
- downloadButton.getElementsByTagName("button").length
309
- ).toBeGreaterThan(0);
423
+ expect(queryByLabelText("Trim video to selection")).toBeNull();
310
424
  });
311
425
 
312
- test.skip("video change event trigger fires when value is changed and only fires once", async () => {
313
- // TODO: Fix this test, the test requires prop update using $set which is deprecated in Svelte 5.
314
- const { component, listen } = await render(Video, {
315
- show_label: true,
316
- loading_status,
426
+ test("clicking trim button enters edit mode with Trim and Cancel buttons", async () => {
427
+ const { getByLabelText, getByText } = await render(Video, {
428
+ ...default_props,
317
429
  interactive: true,
318
- value: [
319
- {
320
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
321
- }
322
- ],
323
- root: "foo",
324
- proxy_url: null,
325
- streaming: false,
326
- pending: false,
327
- sources: ["upload"],
328
- autoplay: true,
329
- webcam_options: {
330
- mirror: true,
331
- constraints: null
332
- }
430
+ value: fake_value
431
+ });
432
+
433
+ await fireEvent.click(getByLabelText("Trim video to selection"));
434
+
435
+ expect(getByText("Trim")).toBeTruthy();
436
+ expect(getByText("Cancel")).toBeTruthy();
437
+ });
438
+
439
+ test("clicking Cancel exits edit mode and restores trim button", async () => {
440
+ const { getByLabelText, getByText, queryByText } = await render(Video, {
441
+ ...default_props,
442
+ interactive: true,
443
+ value: fake_value
444
+ });
445
+
446
+ await fireEvent.click(getByLabelText("Trim video to selection"));
447
+ expect(getByText("Cancel")).toBeTruthy();
448
+
449
+ await fireEvent.click(getByText("Cancel"));
450
+
451
+ expect(queryByText("Cancel")).toBeNull();
452
+ expect(getByLabelText("Trim video to selection")).toBeTruthy();
453
+ });
454
+
455
+ test("time display is rendered", async () => {
456
+ const { container } = await render(Video, {
457
+ ...default_props,
458
+ interactive: true,
459
+ value: fake_value
460
+ });
461
+
462
+ const timeDisplay = container.querySelector(".time");
463
+ expect(timeDisplay).toBeTruthy();
464
+ });
465
+ });
466
+
467
+ const upload_props = {
468
+ ...default_props,
469
+ sources: ["upload"] as "upload"[],
470
+ interactive: true,
471
+ value: null,
472
+ root: "https://example.com",
473
+ client: mock_client()
474
+ };
475
+
476
+ describe("Events: upload via file input", () => {
477
+ setupi18n();
478
+ afterEach(() => cleanup());
479
+
480
+ test("selecting a file triggers upload, change, and input events", async () => {
481
+ const { listen } = await render(Video, upload_props);
482
+
483
+ const upload = listen("upload");
484
+ const change = listen("change");
485
+ const input = listen("input");
486
+
487
+ await upload_file(TEST_MP4);
488
+
489
+ await waitFor(() => {
490
+ expect(upload).toHaveBeenCalledTimes(1);
333
491
  });
492
+ expect(input).toHaveBeenCalledTimes(1);
493
+ expect(change).toHaveBeenCalledTimes(1);
494
+ });
495
+
496
+ test("drag and drop a file triggers upload, change, and input events", async () => {
497
+ const { listen } = await render(Video, upload_props);
334
498
 
335
- const mock = listen("change");
499
+ const upload = listen("upload");
500
+ const change = listen("change");
501
+ const input = listen("input");
336
502
 
337
- ((component.value = [
338
- {
339
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/b.mp4"
503
+ await drop_file(TEST_MP4, "[aria-label='video.drop_to_upload']");
504
+
505
+ await waitFor(() => {
506
+ expect(upload).toHaveBeenCalledTimes(1);
507
+ });
508
+ expect(input).toHaveBeenCalledTimes(1);
509
+ expect(change).toHaveBeenCalledTimes(1);
510
+ });
511
+
512
+ test("upload failure dispatches error event with the message", async () => {
513
+ const failing_upload = vi
514
+ .fn()
515
+ .mockRejectedValue(new Error("File too large"));
516
+ const { listen } = await render(Video, {
517
+ ...upload_props,
518
+ client: {
519
+ upload: failing_upload,
520
+ stream: async () => ({ onmessage: null, close: () => {} })
340
521
  }
341
- ]),
342
- assert.equal(mock.callCount, 1));
522
+ });
523
+
524
+ const error = listen("error");
525
+
526
+ await upload_file(TEST_MP4);
527
+
528
+ await waitFor(() => {
529
+ expect(failing_upload).toHaveBeenCalled();
530
+ });
531
+
532
+ await waitFor(() => {
533
+ expect(error).toHaveBeenCalledTimes(1);
534
+ });
535
+ expect(error).toHaveBeenCalledWith("File too large");
343
536
  });
344
537
  });
package/dist/Index.svelte CHANGED
@@ -27,7 +27,8 @@
27
27
  }
28
28
 
29
29
  const gradio = new VideoGradio(props);
30
- let old_value = $state(gradio.props.value);
30
+ let old_value = gradio.props.value;
31
+ let mounted = false;
31
32
 
32
33
  let uploading = $state(false);
33
34
  let dragging = $state(false);
@@ -37,6 +38,11 @@
37
38
  let initial_value: FileData | null = gradio.props.value;
38
39
 
39
40
  $effect(() => {
41
+ if (!mounted) {
42
+ old_value = gradio.props.value;
43
+ mounted = true;
44
+ return;
45
+ }
40
46
  if (old_value != gradio.props.value) {
41
47
  old_value = gradio.props.value;
42
48
  gradio.dispatch("change");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/video",
3
- "version": "0.20.4",
3
+ "version": "0.20.6",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -11,16 +11,16 @@
11
11
  "@ffmpeg/util": "^0.12.2",
12
12
  "hls.js": "^1.6.13",
13
13
  "mrmime": "^2.0.1",
14
- "@gradio/atoms": "^0.22.2",
14
+ "@gradio/atoms": "^0.23.0",
15
+ "@gradio/image": "^0.26.1",
15
16
  "@gradio/client": "^2.1.0",
16
- "@gradio/image": "^0.25.4",
17
17
  "@gradio/icons": "^0.15.1",
18
- "@gradio/statustracker": "^0.12.5",
19
- "@gradio/utils": "^0.12.0",
20
- "@gradio/upload": "^0.17.7"
18
+ "@gradio/statustracker": "^0.13.1",
19
+ "@gradio/upload": "^0.17.8",
20
+ "@gradio/utils": "^0.12.2"
21
21
  },
22
22
  "devDependencies": {
23
- "@gradio/preview": "^0.16.0"
23
+ "@gradio/preview": "^0.16.2"
24
24
  },
25
25
  "exports": {
26
26
  "./package.json": "./package.json",