Repository: gradio-app/gradio

Issue:
Video Unit tests
@pngwn taking care of this in #4705


Discussion:

Resolving patch:
diff --git a/demo/audio_component/run.py b/demo/audio_component/run.py
--- a/demo/audio_component/run.py
+++ b/demo/audio_component/run.py
@@ -5,4 +5,4 @@
 with gr.Blocks(css=css) as demo:
     gr.Audio()
 
-demo.launch()
+demo.launch()
\ No newline at end of file


Test patch:
diff --git a/js/app/src/components/Audio/Audio.test.ts b/js/app/src/components/Audio/Audio.test.ts
--- a/js/app/src/components/Audio/Audio.test.ts
+++ b/js/app/src/components/Audio/Audio.test.ts
@@ -1,7 +1,9 @@
-import { test, describe, assert, afterEach, vi } from "vitest";
-import { cleanup, render, wait_for_event } from "@gradio/tootils";
+import { test, describe, assert, afterEach, vi, beforeAll } from "vitest";
+import { spy, spyOn } from "tinyspy";
+import { cleanup, render, wait_for_event, wait } from "@gradio/tootils";
 import event from "@testing-library/user-event";
 import { setupi18n } from "../../i18n";
+import { tick } from "svelte";
 
 import Audio from "./Audio.svelte";
 import type { LoadingStatus } from "../StatusTracker/types";
@@ -18,10 +20,14 @@ const loading_status = {
 };
 
 describe("Audio", () => {
+	beforeAll(() => {
+		window.HTMLMediaElement.prototype.play = vi.fn();
+		window.HTMLMediaElement.prototype.pause = vi.fn();
+	});
 	afterEach(() => cleanup());
 
 	test("renders provided value and label", async () => {
-		const { getByTestId, queryAllByText } = render(Audio, {
+		const { getByTestId, queryAllByText } = await render(Audio, {
 			show_label: true,
 			loading_status,
 			mode: "dynamic",
@@ -40,7 +46,7 @@ describe("Audio", () => {
 		});
 
 		assert.isTrue(
-			getByTestId("Audio Component-dynamic-audio").src.endsWith(
+			getByTestId("Audio Component-audio").src.endsWith(
 				"foo/file=https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav"
 			)
 		);
@@ -48,7 +54,7 @@ describe("Audio", () => {
 	});
 
 	test("hides label", async () => {
-		const { queryAllByText } = render(Audio, {
+		const { queryAllByText } = await render(Audio, {
 			show_label: false,
 			loading_status,
 			mode: "dynamic",
@@ -71,7 +77,7 @@ describe("Audio", () => {
 
 	test("upload sets change event", async () => {
 		setupi18n();
-		const { container, component } = render(Audio, {
+		const { container, component } = await render(Audio, {
 			show_label: false,
 			loading_status,
 			value: null,
@@ -97,7 +103,7 @@ describe("Audio", () => {
 	});
 
 	test("static audio sets value", async () => {
-		const { getByTestId } = render(Audio, {
+		const { getByTestId } = await render(Audio, {
 			show_label: true,
 			loading_status,
 			mode: "static",
@@ -116,7 +122,7 @@ describe("Audio", () => {
 		});
 
 		assert.isTrue(
-			getByTestId("Audio Component-static-audio").src.endsWith(
+			getByTestId("Audio Component-audio").src.endsWith(
 				"foo/file=https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav"
 			)
 		);
@@ -158,7 +164,7 @@ describe("Audio", () => {
 		vi.stubGlobal("navigator", media_mock);
 		vi.stubGlobal("MediaRecorder", media_recorder_mock);
 
-		const { component, getByText } = render(Audio, {
+		const { component, getByText } = await render(Audio, {
 			show_label: true,
 			loading_status,
 			mode: "dynamic",
@@ -168,7 +174,8 @@ describe("Audio", () => {
 			root_url: null,
 			streaming: false,
 			pending: false,
-			source: "microphone"
+			source: "microphone",
+			name: "bar"
 		});
 
 		const startButton = getByText("Record from microphone");
@@ -184,4 +191,130 @@ describe("Audio", () => {
 		assert.equal(component.$capture_state().value.name, "audio.wav");
 		assert.equal(mock.callCount, 1);
 	});
+
+	test("when autoplay is true `media.play` should be called in static mode", async () => {
+		const { getByTestId } = await render(Audio, {
+			show_label: true,
+			loading_status,
+			mode: "static",
+			value: {
+				name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
+				data: null,
+				is_file: true
+			},
+			label: "static",
+			root: "foo",
+			root_url: null,
+			streaming: false,
+			pending: false,
+			source: "microphone",
+			autoplay: true
+		});
+
+		const startButton = getByTestId<HTMLAudioElement>("static-audio");
+		const fn = spyOn(startButton, "play");
+		startButton.dispatchEvent(new Event("loadeddata"));
+
+		assert.equal(fn.callCount, 1);
+	});
+
+	test("when autoplay is true `media.play` should be called in dynamic mode", async () => {
+		const { getByTestId } = await render(Audio, {
+			show_label: true,
+			loading_status,
+			mode: "dynamic",
+			value: {
+				name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
+				data: null,
+				is_file: true
+			},
+			label: "dynamic",
+			root: "foo",
+			root_url: null,
+			streaming: false,
+			pending: false,
+			source: "microphone",
+			autoplay: true
+		});
+
+		const startButton = getByTestId<HTMLAudioElement>("dynamic-audio");
+		const fn = spyOn(startButton, "play");
+		startButton.dispatchEvent(new Event("loadeddata"));
+
+		assert.equal(fn.callCount, 1);
+	});
+
+	test("when autoplay is true `media.play` should be called in static mode when the audio data is updated", async () => {
+		const { component, getByTestId } = await render(Audio, {
+			show_label: true,
+			loading_status,
+			mode: "static",
+			value: {
+				name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
+				data: null,
+				is_file: true
+			},
+			label: "static",
+			root: "foo",
+			root_url: null,
+			streaming: false,
+			pending: false,
+			source: "microphone",
+			autoplay: true
+		});
+
+		const startButton = getByTestId<HTMLAudioElement>("static-audio");
+		const fn = spyOn(startButton, "play");
+
+		startButton.dispatchEvent(new Event("loadeddata"));
+
+		component.$set({
+			value: {
+				name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
+				data: null,
+				is_file: true
+			}
+		});
+
+		startButton.dispatchEvent(new Event("loadeddata"));
+
+		assert.equal(fn.callCount, 2);
+	});
+
+	test("when autoplay is true `media.play` should be called in dynamic mode when the audio data is updated", async () => {
+		const { component, getByTestId } = await render(Audio, {
+			show_label: true,
+			loading_status,
+			mode: "dynamic",
+			value: {
+				name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
+				data: null,
+				is_file: true
+			},
+			label: "dynamic",
+			root: "foo",
+			root_url: null,
+			streaming: false,
+			pending: false,
+			source: "microphone",
+			autoplay: true
+		});
+
+		const startButton = getByTestId<HTMLAudioElement>("dynamic-audio");
+		const fn = spyOn(startButton, "play");
+
+		startButton.dispatchEvent(new Event("loadeddata"));
+
+		component.$set({
+			value: {
+				name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
+				data: null,
+				is_file: true
+			}
+		});
+
+		startButton.dispatchEvent(new Event("loadeddata"));
+
+		assert.equal(fn.callCount, 2);
+	});
 });
diff --git a/js/app/src/components/Button/Button.test.ts b/js/app/src/components/Button/Button.test.ts
--- a/js/app/src/components/Button/Button.test.ts
+++ b/js/app/src/components/Button/Button.test.ts
@@ -7,13 +7,17 @@ import Button from "./Button.svelte";
 describe("Hello.svelte", () => {
 	afterEach(() => cleanup());
 
-	test.skip("renders label text", () => {
-		const { container, component } = render(Button, { value: "Click Me" });
+	test.skip("renders label text", async () => {
+		const { container, component } = await render(Button, {
+			value: "Click Me"
+		});
 		assert.equal(container.innerText, "Click Me");
 	});
 
 	test.skip("triggers callback when clicked", async () => {
-		const { container, component } = render(Button, { value: "Click Me" });
+		const { container, component } = await render(Button, {
+			value: "Click Me"
+		});
 		const mock = spy();
 		component.$on("click", mock);
 
diff --git a/js/app/src/components/Chatbot/Chatbot.test.ts b/js/app/src/components/Chatbot/Chatbot.test.ts
--- a/js/app/src/components/Chatbot/Chatbot.test.ts
+++ b/js/app/src/components/Chatbot/Chatbot.test.ts
@@ -19,7 +19,7 @@ describe.skip("Chatbot", () => {
 	afterEach(() => cleanup());
 
 	test("renders user and bot messages", async () => {
-		const { getAllByTestId } = render(Chatbot, {
+		const { getAllByTestId } = await render(Chatbot, {
 			loading_status,
 			label: "hello",
 			value: [["user message one", "bot message one"]],
@@ -37,7 +37,7 @@ describe.skip("Chatbot", () => {
 	});
 
 	test("renders additional message as they are passed", async () => {
-		const { component, getAllByTestId } = render(Chatbot, {
+		const { component, getAllByTestId } = await render(Chatbot, {
 			loading_status,
 			label: "hello",
 			value: [["user message one", "bot message one"]],
diff --git a/js/app/src/components/ColorPicker/ColorPicker.test.ts b/js/app/src/components/ColorPicker/ColorPicker.test.ts
--- a/js/app/src/components/ColorPicker/ColorPicker.test.ts
+++ b/js/app/src/components/ColorPicker/ColorPicker.test.ts
@@ -17,8 +17,8 @@ const loading_status = {
 describe("ColorPicker", () => {
 	afterEach(() => cleanup());
 
-	test("renders provided value", () => {
-		const { getByDisplayValue } = render(ColorPicker, {
+	test("renders provided value", async () => {
+		const { getByDisplayValue } = await render(ColorPicker, {
 			loading_status,
 			show_label: true,
 			mode: "dynamic",
@@ -31,7 +31,7 @@ describe("ColorPicker", () => {
 	});
 
 	test("changing the color should update the value", async () => {
-		const { component, getByDisplayValue } = render(ColorPicker, {
+		const { component, getByDisplayValue } = await render(ColorPicker, {
 			loading_status,
 			show_label: true,
 			mode: "dynamic",
diff --git a/js/app/src/components/Gallery/Gallery.test.ts b/js/app/src/components/Gallery/Gallery.test.ts
--- a/js/app/src/components/Gallery/Gallery.test.ts
+++ b/js/app/src/components/Gallery/Gallery.test.ts
@@ -4,14 +4,15 @@ import { cleanup, render } from "@gradio/tootils";
 import Gallery from "./Gallery.svelte";
 import type { LoadingStatus } from "../StatusTracker/types";
 
-const loading_status = {
+const loading_status: LoadingStatus = {
 	eta: 0,
 	queue_position: 1,
 	queue_size: 1,
-	status: "complete" as LoadingStatus["status"],
+	status: "complete",
 	scroll_to_output: false,
 	visible: true,
-	fn_index: 0
+	fn_index: 0,
+	show_progress: "full"
 };
 
 describe("Gallery", () => {
@@ -21,7 +22,9 @@ describe("Gallery", () => {
 	});
 
 	test("preview shows detailed image by default", async () => {
-		const { getAllByTestId, getByTestId } = render(Gallery, {
+		window.Element.prototype.scrollTo = vi.fn(() => {});
+
+		const { getAllByTestId, getByTestId } = await render(Gallery, {
 			loading_status,
 			label: "gallery",
 			// @ts-ignore
@@ -51,14 +54,12 @@ describe("Gallery", () => {
 		});
 
 		const details = getAllByTestId("detailed-image");
-		const container = getByTestId("container_el");
-		container.scrollTo = () => {};
 
 		assert.equal(details.length, 1);
 	});
 
 	test("detailed view does not show larger image", async () => {
-		const { queryAllByTestId, getByTestId } = render(Gallery, {
+		const { queryAllByTestId, getByTestId } = await render(Gallery, {
 			loading_status,
 			label: "gallery",
 			// @ts-ignore
diff --git a/js/app/src/components/Radio/Radio.test.ts b/js/app/src/components/Radio/Radio.test.ts
--- a/js/app/src/components/Radio/Radio.test.ts
+++ b/js/app/src/components/Radio/Radio.test.ts
@@ -21,8 +21,8 @@ describe("Radio", () => {
 	afterEach(() => cleanup());
 	const choices = ["dog", "cat", "turtle"];
 
-	test("renders provided value", () => {
-		const { getAllByRole, getByTestId } = render(Radio, {
+	test("renders provided value", async () => {
+		const { getAllByRole, getByTestId } = await render(Radio, {
 			show_label: true,
 			loading_status,
 			choices: choices,
@@ -46,7 +46,7 @@ describe("Radio", () => {
 	});
 
 	test("should update the value when a radio is clicked", async () => {
-		const { getByDisplayValue, getByTestId } = render(Radio, {
+		const { getByDisplayValue, getByTestId } = await render(Radio, {
 			show_label: true,
 			loading_status,
 			choices: choices,
diff --git a/js/app/src/components/Textbox/Textbox.test.ts b/js/app/src/components/Textbox/Textbox.test.ts
--- a/js/app/src/components/Textbox/Textbox.test.ts
+++ b/js/app/src/components/Textbox/Textbox.test.ts
@@ -19,8 +19,8 @@ const loading_status = {
 describe("Textbox", () => {
 	afterEach(() => cleanup());
 
-	test("renders provided value", () => {
-		const { getByDisplayValue } = render(Textbox, {
+	test("renders provided value", async () => {
+		const { getByDisplayValue } = await render(Textbox, {
 			show_label: true,
 			max_lines: 1,
 			loading_status,
@@ -35,7 +35,7 @@ describe("Textbox", () => {
 	});
 
 	test("changing the text should update the value", async () => {
-		const { component, getByDisplayValue } = render(Textbox, {
+		const { component, getByDisplayValue } = await render(Textbox, {
 			show_label: true,
 			max_lines: 10,
 			loading_status,
diff --git a/js/app/src/components/Video/Video.test.ts b/js/app/src/components/Video/Video.test.ts
new file mode 100644
--- /dev/null
+++ b/js/app/src/components/Video/Video.test.ts
@@ -0,0 +1,251 @@
+import {
+	test,
+	describe,
+	assert,
+	afterEach,
+	vi,
+	beforeAll,
+	beforeEach,
+	expect
+} from "vitest";
+import { spyOn } from "tinyspy";
+import { cleanup, render } from "@gradio/tootils";
+import { setupi18n } from "../../i18n";
+
+import Video from "./Video.svelte";
+import type { LoadingStatus } from "../StatusTracker/types";
+
+const loading_status = {
+	eta: 0,
+	queue_position: 1,
+	queue_size: 1,
+	status: "complete" as LoadingStatus["status"],
+	scroll_to_output: false,
+	visible: true,
+	fn_index: 0,
+	show_progress: "full" as LoadingStatus["show_progress"]
+};
+
+describe("Video", () => {
+	beforeAll(() => {
+		window.HTMLMediaElement.prototype.play = vi.fn();
+		window.HTMLMediaElement.prototype.pause = vi.fn();
+	});
+	beforeEach(setupi18n);
+	afterEach(() => cleanup());
+
+	test("renders provided value and label", async () => {
+		const { getByTestId, queryAllByText } = await render(Video, {
+			show_label: true,
+			loading_status,
+			mode: "dynamic",
+			value: [
+				{
+					name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
+					data: null,
+					is_file: true
+				}
+			],
+			label: "Test Label",
+			root: "foo",
+			root_url: null,
+			streaming: false,
+			pending: false,
+			name: "bar",
+			source: "upload"
+		});
+		assert.isTrue(
+			getByTestId("Test Label-player").src.endsWith(
+				"foo/file=https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav"
+			)
+		);
+		assert(queryAllByText("Test Label").length, 1);
+	});
+
+	test("hides label", async () => {
+		const { queryAllByText } = await render(Video, {
+			show_label: false,
+			loading_status,
+			mode: "dynamic",
+			value: {
+				name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
+				data: null,
+				is_file: true
+			},
+			label: "Video Component",
+			root: "foo",
+			root_url: null,
+			streaming: false,
+			pending: false,
+			name: "bar",
+			source: "upload"
+		});
+		assert(queryAllByText("Video Component").length, 0);
+	});
+
+	test("static Video sets value", async () => {
+		const { getByTestId } = await render(Video, {
+			show_label: true,
+			loading_status,
+			mode: "static",
+			value: [
+				{
+					name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
+					data: null,
+					is_file: true
+				}
+			],
+			root: "foo",
+			root_url: null,
+			streaming: false,
+			pending: false,
+			name: "bar",
+			source: "upload"
+		});
+		assert.isTrue(
+			getByTestId("test-player").src.endsWith(
+				"foo/file=https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav"
+			)
+		);
+	});
+
+	test("when autoplay is true `media.play` should be called in static mode", async () => {
+		const { getByTestId } = await render(Video, {
+			show_label: true,
+			loading_status,
+			mode: "static",
+			value: [
+				{
+					name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
+					data: null,
+					is_file: true
+				}
+			],
+			root: "foo",
+			root_url: null,
+			streaming: false,
+			pending: false,
+			source: "microphone",
+			autoplay: true
+		});
+		const startButton = getByTestId<HTMLAudioElement>("test-player");
+		const fn = spyOn(startButton, "play");
+		startButton.dispatchEvent(new Event("loadeddata"));
+		assert.equal(fn.callCount, 1);
+	});
+
+	test("when autoplay is true `media.play` should be called in dynamic mode", async () => {
+		const { getByTestId } = await render(Video, {
+			show_label: true,
+			loading_status,
+			mode: "dynamic",
+			value: [
+				{
+					name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
+					data: null,
+					is_file: true
+				}
+			],
+			root: "foo",
+			root_url: null,
+			streaming: false,
+			pending: false,
+			source: "microphone",
+			autoplay: true
+		});
+		const startButton = getByTestId<HTMLAudioElement>("test-player");
+		const fn = spyOn(startButton, "play");
+		startButton.dispatchEvent(new Event("loadeddata"));
+		assert.equal(fn.callCount, 1);
+	});
+
+	test("when autoplay is true `media.play` should be called in static mode when the Video data is updated", async () => {
+		const { component, getByTestId } = await render(Video, {
+			show_label: true,
+			loading_status,
+			mode: "static",
+			value: [
+				{
+					name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
+					data: null,
+					is_file: true
+				}
+			],
+			root: "foo",
+			root_url: null,
+			streaming: false,
+			pending: false,
+			source: "microphone",
+			autoplay: true
+		});
+		const startButton = getByTestId<HTMLAudioElement>("test-player");
+		const fn = spyOn(startButton, "play");
+		startButton.dispatchEvent(new Event("loadeddata"));
+		component.$set({
+			value: {
+				name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
+				data: null,
+				is_file: true
+			}
+		});
+		startButton.dispatchEvent(new Event("loadeddata"));
+		assert.equal(fn.callCount, 2);
+	});
+
+	test("when autoplay is true `media.play` should be called in dynamic mode when the Video data is updated", async () => {
+		const { component, getByTestId } = await render(Video, {
+			show_label: true,
+			loading_status,
+			mode: "dynamic",
+			value: [
+				{
+					name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
+					data: null,
+					is_file: true
+				}
+			],
+			root: "foo",
+			root_url: null,
+			streaming: false,
+			pending: false,
+			source: "microphone",
+			autoplay: true
+		});
+		const startButton = getByTestId<HTMLAudioElement>("test-player");
+		const fn = spyOn(startButton, "play");
+		startButton.dispatchEvent(new Event("loadeddata"));
+		component.$set({
+			value: {
+				name: "https://gradio-builds.s3.amazonaws.com/demo-files/audio_sample.wav",
+				data: null,
+				is_file: true
+			}
+		});
+		startButton.dispatchEvent(new Event("loadeddata"));
+		assert.equal(fn.callCount, 2);
+	});
+	test("renders video and download button", async () => {
+		const data = [
+			{
+				data: null,
+				name: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
+				is_file: true
+			}
+		];
+		const results = await render(Video, {
+			mode: "static",
+			label: "video",
+			show_label: true,
+			value: data,
+			root: "foo"
+		});
+
+		const downloadButton = results.getAllByTestId("download-div")[0];
+		expect(
+			downloadButton.getElementsByTagName("a")[0].getAttribute("href")
+		).toBe(`foo/file=${data[0].name}`);
+		expect(
+			downloadButton.getElementsByTagName("button").length
+		).toBeGreaterThan(0);
+	});
+});
diff --git a/js/form/src/Range.test.ts b/js/form/src/Range.test.ts
--- a/js/form/src/Range.test.ts
+++ b/js/form/src/Range.test.ts
@@ -6,8 +6,8 @@ import Range from "./Range.svelte";
 describe("Range", () => {
 	afterEach(() => cleanup());
 
-	test("Release event called on blur and pointerUp", () => {
-		const results = render(Range, {
+	test("Release event called on blur and pointerUp", async () => {
+		const results = await render(Range, {
 			label: "range",
 			show_label: true,
 			value: 1,
diff --git a/js/video/src/StaticVideo.test.ts b/js/video/src/StaticVideo.test.ts
deleted file mode 100644
--- a/js/video/src/StaticVideo.test.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import "@testing-library/jest-dom";
-
-import { test, describe, afterEach } from "vitest";
-import { cleanup, render } from "@gradio/tootils";
-
-import StaticVideo from "./StaticVideo.svelte";
-
-describe("StaticVideo", () => {
-	afterEach(() => cleanup());
-
-	const data = {
-		data: "https://raw.githubusercontent.com/gradio-app/gradio/main/gradio/demo/video_component/files/a.mp4",
-		name: "a.mp4"
-	};
-
-	test("renders video and download button", () => {
-		const results = render(StaticVideo, {
-			label: "video",
-			show_label: true,
-			value: data
-		});
-
-		//expect(results.getAllByLabelText("video")).not.toThrow();
-
-		const downloadButton = results.getAllByTestId("download-div")[0];
-		expect(
-			downloadButton.getElementsByTagName("a")[0].getAttribute("href")
-		).toBe(data.data);
-		expect(
-			downloadButton.getElementsByTagName("button").length
-		).toBeGreaterThan(0);
-
-		expect(downloadButton.getElementsByTagName("button")[0]).toBeVisible();
-	});
-});