@gradio/video 0.20.5 → 0.20.7

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,28 @@
1
1
  # @gradio/video
2
2
 
3
+ ## 0.20.7
4
+
5
+ ### Dependency updates
6
+
7
+ - @gradio/atoms@0.23.1
8
+ - @gradio/statustracker@0.14.0
9
+ - @gradio/client@2.2.0
10
+ - @gradio/image@0.26.2
11
+
12
+ ## 0.20.6
13
+
14
+ ### Features
15
+
16
+ - [#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!
17
+
18
+ ### Dependency updates
19
+
20
+ - @gradio/utils@0.12.2
21
+ - @gradio/atoms@0.23.0
22
+ - @gradio/statustracker@0.13.1
23
+ - @gradio/upload@0.17.8
24
+ - @gradio/image@0.26.1
25
+
3
26
  ## 0.20.5
4
27
 
5
28
  ### 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,10 +5,19 @@ import {
5
5
  afterEach,
6
6
  vi,
7
7
  beforeAll,
8
- beforeEach,
9
8
  expect
10
9
  } from "vitest";
11
- import { cleanup, render } from "@self/tootils/render";
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";
12
21
  import { setupi18n } from "../core/src/i18n";
13
22
 
14
23
  vi.mock("@ffmpeg/ffmpeg", () => ({
@@ -28,314 +37,501 @@ vi.mock("@ffmpeg/util", () => ({
28
37
  }));
29
38
 
30
39
  import Video from "./Index.svelte";
31
-
32
40
  import type { LoadingStatus } from "@gradio/statustracker";
33
41
 
34
- const loading_status = {
42
+ const loading_status: LoadingStatus = {
35
43
  eta: 0,
36
44
  queue_position: 1,
37
45
  queue_size: 1,
38
- status: "complete" as LoadingStatus["status"],
46
+ status: "complete",
39
47
  scroll_to_output: false,
40
48
  visible: true,
41
49
  fn_index: 0,
42
- 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 })[]
43
77
  };
44
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
+
45
94
  describe("Video", () => {
46
- beforeAll(() => {
47
- window.HTMLMediaElement.prototype.play = vi.fn();
48
- 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");
49
118
  });
50
- beforeEach(setupi18n);
119
+ });
120
+
121
+ describe("Props: sources", () => {
122
+ setupi18n();
51
123
  afterEach(() => cleanup());
52
124
 
53
- test("renders provided value and label", async () => {
54
- const { getByTestId, queryAllByText } = await render(Video, {
55
- show_label: true,
56
- loading_status,
57
- value: {
58
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
59
- url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
60
- },
61
- label: "Test Label",
62
- root: "foo",
63
- proxy_url: null,
64
- streaming: false,
65
- pending: false,
66
- 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,
67
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,
68
293
  interactive: true,
69
- webcam_options: {
70
- mirror: true,
71
- constraints: null
72
- }
294
+ value: fake_value
73
295
  });
74
- let vid = getByTestId("Test Label-player") as HTMLVideoElement;
75
- assert.equal(
76
- vid.src,
77
- "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
78
- );
79
- assert.equal(queryAllByText("Test Label").length, 1);
296
+
297
+ const clearBtn = getByLabelText("common.clear");
298
+ expect(clearBtn).toBeTruthy();
80
299
  });
81
300
 
82
- test("hides label", async () => {
83
- const { queryAllByText } = await render(Video, {
84
- show_label: false,
85
- loading_status,
86
- value: {
87
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
88
- url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
89
- },
90
- label: "Video Component",
91
- root: "foo",
92
- proxy_url: null,
93
- streaming: false,
94
- pending: false,
95
- name: "bar",
96
- 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,
97
304
  interactive: true,
98
- webcam_options: {
99
- mirror: true,
100
- constraints: null
101
- }
305
+ value: fake_value
102
306
  });
103
- 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);
104
316
  });
317
+ });
105
318
 
106
- test("static Video sets value", async () => {
107
- const { getByTestId } = await render(Video, {
108
- show_label: true,
109
- loading_status,
110
- value: {
111
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
112
- url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
113
- },
114
- root: "foo",
115
- proxy_url: null,
116
- streaming: false,
117
- pending: false,
118
- name: "bar",
119
- sources: ["upload"],
120
- mode: "static",
121
- webcam_options: {
122
- mirror: true,
123
- constraints: null
124
- }
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
125
327
  });
126
- let vid = getByTestId("test-player") as HTMLVideoElement;
127
- assert.equal(
128
- vid.src,
129
- "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
130
- );
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);
131
336
  });
337
+ });
338
+
339
+ describe("Autoplay", () => {
340
+ setupi18n();
341
+ afterEach(() => cleanup());
132
342
 
133
- test("when autoplay is true `media.play` should be called in static mode", async () => {
343
+ test("autoplay calls media.play in static mode", async () => {
134
344
  const { getByTestId } = await render(Video, {
135
- show_label: true,
136
- loading_status,
345
+ ...default_props,
137
346
  interactive: false,
138
- value: {
139
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
140
- url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
141
- },
142
- root: "foo",
143
- proxy_url: null,
144
- streaming: false,
145
- pending: false,
146
- sources: ["upload"],
147
- autoplay: true,
148
- webcam_options: {
149
- mirror: true,
150
- constraints: null
151
- }
347
+ value: fake_value,
348
+ autoplay: true
152
349
  });
153
- const startButton = getByTestId("test-player") as HTMLVideoElement;
154
- const fn = vi.spyOn(startButton, "play").mockResolvedValue(undefined);
155
- startButton.dispatchEvent(new Event("loadeddata"));
156
- assert.equal(fn.mock.calls.length, 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);
157
355
  });
158
356
 
159
- test("when autoplay is true `media.play` should be called in dynamic mode", async () => {
357
+ test("autoplay calls media.play in interactive mode", async () => {
160
358
  const { getByTestId } = await render(Video, {
161
- show_label: true,
162
- loading_status,
163
- value: {
164
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
165
- url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
166
- },
167
- root: "foo",
168
- proxy_url: null,
169
- streaming: false,
170
- pending: false,
171
- sources: ["upload"],
172
- autoplay: true,
173
- webcam_options: {
174
- mirror: true,
175
- constraints: null
176
- }
359
+ ...default_props,
360
+ interactive: true,
361
+ value: fake_value,
362
+ autoplay: true
177
363
  });
178
364
 
179
- const video_player = getByTestId("test-player") as HTMLVideoElement;
180
- const fn = vi.spyOn(video_player, "play").mockResolvedValue(undefined);
181
- assert.equal(fn.mock.calls.length, 1);
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();
182
369
  });
370
+ });
183
371
 
184
- test("when autoplay is true `media.play` should be called in static mode when the Video data is updated", async () => {
185
- const { getByTestId, unmount } = await render(Video, {
186
- show_label: true,
187
- loading_status,
188
- interactive: false,
189
- value: {
190
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
191
- url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
192
- },
193
- root: "foo",
194
- proxy_url: null,
195
- streaming: false,
196
- pending: false,
197
- sources: ["upload"],
198
- autoplay: true,
199
- webcam_options: {
200
- mirror: true,
201
- constraints: null
202
- }
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
203
381
  });
204
- let video_player = getByTestId("test-player") as HTMLVideoElement;
205
- const fn = vi.spyOn(video_player, "play").mockResolvedValue(undefined);
206
- assert.equal(fn.mock.calls.length, 1);
207
- unmount();
208
382
 
209
- const result = await render(Video, {
210
- show_label: true,
211
- loading_status,
212
- interactive: false,
213
- value: {
214
- path: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
215
- url: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav"
216
- },
217
- root: "foo",
218
- proxy_url: null,
219
- streaming: false,
220
- pending: false,
221
- sources: ["upload"],
222
- autoplay: true,
223
- webcam_options: {
224
- mirror: true,
225
- constraints: null
226
- }
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
227
391
  });
228
- video_player = result.getByTestId("test-player") as HTMLVideoElement;
229
- const fn2 = vi.spyOn(video_player, "play").mockResolvedValue(undefined);
230
- assert.equal(fn2.mock.calls.length, 1);
392
+
393
+ expect(getByLabelText("Adjust volume")).toBeTruthy();
231
394
  });
232
395
 
233
- test("when autoplay is true `media.play` should be called in dynamic mode when the Video data is updated", async () => {
234
- const { getByTestId, unmount } = await render(Video, {
235
- show_label: true,
236
- loading_status,
396
+ test("fullscreen button is rendered", async () => {
397
+ const { getByLabelText } = await render(Video, {
398
+ ...default_props,
237
399
  interactive: true,
238
- value: {
239
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
240
- url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
241
- },
242
- root: "foo",
243
- proxy_url: null,
244
- streaming: false,
245
- pending: false,
246
- sources: ["upload"],
247
- autoplay: true,
248
- webcam_options: {
249
- mirror: true,
250
- constraints: null
251
- }
400
+ value: fake_value
252
401
  });
253
- let video_player = getByTestId("test-player") as HTMLVideoElement;
254
- const fn = vi.spyOn(video_player, "play").mockResolvedValue(undefined);
255
- assert.equal(fn.mock.calls.length, 1);
256
- unmount();
257
402
 
258
- const result = await render(Video, {
259
- show_label: true,
260
- 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,
261
409
  interactive: true,
262
- value: {
263
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
264
- url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
265
- },
266
- root: "foo",
267
- proxy_url: null,
268
- streaming: false,
269
- pending: false,
270
- sources: ["upload"],
271
- autoplay: true,
272
- webcam_options: {
273
- mirror: true,
274
- constraints: null
275
- }
410
+ value: fake_value
276
411
  });
277
- video_player = result.getByTestId("test-player") as HTMLVideoElement;
278
- const fnResult = vi
279
- .spyOn(video_player, "play")
280
- .mockResolvedValue(undefined);
281
- assert.equal(fnResult.mock.calls.length, 1);
412
+
413
+ expect(getByLabelText("Trim video to selection")).toBeTruthy();
282
414
  });
283
- test("renders video and download button", async () => {
284
- const data = {
285
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
286
- url: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
287
- };
288
- 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,
289
419
  interactive: false,
290
- label: "video",
291
- show_label: true,
292
- value: data,
293
- root: "https://localhost:8000",
294
- webcam_options: {
295
- mirror: true,
296
- constraints: null
297
- }
420
+ value: fake_value
298
421
  });
299
422
 
300
- const downloadButton = results.getAllByTestId("download-div")[0];
301
- expect(
302
- downloadButton.getElementsByTagName("a")[0].getAttribute("href")
303
- ).toBe(data.path);
304
- expect(
305
- downloadButton.getElementsByTagName("button").length
306
- ).toBeGreaterThan(0);
423
+ expect(queryByLabelText("Trim video to selection")).toBeNull();
307
424
  });
308
425
 
309
- test.skip("video change event trigger fires when value is changed and only fires once", async () => {
310
- // TODO: Fix this test, the test requires prop update using $set which is deprecated in Svelte 5.
311
- const { component, listen } = await render(Video, {
312
- show_label: true,
313
- 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,
314
429
  interactive: true,
315
- value: [
316
- {
317
- path: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4"
318
- }
319
- ],
320
- root: "foo",
321
- proxy_url: null,
322
- streaming: false,
323
- pending: false,
324
- sources: ["upload"],
325
- autoplay: true,
326
- webcam_options: {
327
- mirror: true,
328
- constraints: null
329
- }
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);
330
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);
331
498
 
332
- const mock = listen("change");
499
+ const upload = listen("upload");
500
+ const change = listen("change");
501
+ const input = listen("input");
333
502
 
334
- ((component.value = [
335
- {
336
- 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: () => {} })
337
521
  }
338
- ]),
339
- 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");
340
536
  });
341
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.5",
3
+ "version": "0.20.7",
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",
15
- "@gradio/client": "^2.1.0",
14
+ "@gradio/client": "^2.2.0",
16
15
  "@gradio/icons": "^0.15.1",
17
- "@gradio/image": "^0.26.0",
18
- "@gradio/statustracker": "^0.13.0",
19
- "@gradio/utils": "^0.12.1",
20
- "@gradio/upload": "^0.17.7"
16
+ "@gradio/image": "^0.26.2",
17
+ "@gradio/statustracker": "^0.14.0",
18
+ "@gradio/upload": "^0.17.8",
19
+ "@gradio/utils": "^0.12.2",
20
+ "@gradio/atoms": "^0.23.1"
21
21
  },
22
22
  "devDependencies": {
23
- "@gradio/preview": "^0.16.1"
23
+ "@gradio/preview": "^0.16.2"
24
24
  },
25
25
  "exports": {
26
26
  "./package.json": "./package.json",