Files
wehub-resource-sync adf0d17497
publish / version_or_publish (push) Has been cancelled
storybook-build / changes (push) Has been cancelled
storybook-build / :storybook-build (push) Has been cancelled
Sync Gradio Skills to Hugging Face / sync-skills (push) Has been cancelled
functional / changes (push) Has been cancelled
functional / build-frontend (push) Has been cancelled
functional / functional-test-SSR=false (push) Has been cancelled
functional / functional-reload (push) Has been cancelled
js / changes (push) Has been cancelled
js / js-test (push) Has been cancelled
docs-build / changes (push) Has been cancelled
docs-build / docs-build (push) Has been cancelled
docs-build / website-build (push) Has been cancelled
functional / functional-test-SSR=true (push) Has been cancelled
hygiene / hygiene-test (push) Has been cancelled
python / changes (push) Has been cancelled
python / build (push) Has been cancelled
python / test-ubuntu-latest-flaky (push) Has been cancelled
python / test-ubuntu-latest-not-flaky (push) Has been cancelled
python / test-windows-latest-flaky (push) Has been cancelled
python / test-windows-latest-not-flaky (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:17:32 +08:00

538 lines
13 KiB
TypeScript

import {
test,
describe,
assert,
afterEach,
vi,
beforeAll,
expect
} from "vitest";
import {
cleanup,
render,
fireEvent,
waitFor,
upload_file,
drop_file,
mock_client,
TEST_MP4
} from "@self/tootils/render";
import { run_shared_prop_tests } from "@self/tootils/shared-prop-tests";
import { setupi18n } from "../core/src/i18n";
vi.mock("@ffmpeg/ffmpeg", () => ({
FFmpeg: class MockFFmpeg {
load = vi.fn(() => Promise.resolve());
writeFile = vi.fn(() => Promise.resolve());
readFile = vi.fn(() => Promise.resolve(new Uint8Array()));
exec = vi.fn(() => Promise.resolve(0));
terminate = vi.fn(() => Promise.resolve());
on = vi.fn();
}
}));
vi.mock("@ffmpeg/util", () => ({
fetchFile: vi.fn(() => Promise.resolve(new Uint8Array())),
toBlobURL: vi.fn(() => Promise.resolve("blob:mock"))
}));
import Video from "./Index.svelte";
import type { ILoadingStatus as LoadingStatus } from "@gradio/statustracker";
const loading_status: LoadingStatus = {
eta: 0,
queue_position: 1,
queue_size: 1,
status: "complete",
scroll_to_output: false,
visible: true,
fn_index: 0,
show_progress: "full",
type: "input" as const,
stream_state: "closed" as const
};
const fake_value = {
path: "video_sample.mp4",
url: "https://example.com/video_sample.mp4",
orig_name: "video_sample.mp4",
size: 261179,
mime_type: "video/mp4",
is_stream: false
};
const default_props = {
loading_status,
label: "Video",
show_label: true,
value: null as any,
sources: ["upload", "webcam"] as ("upload" | "webcam")[],
interactive: true,
streaming: false,
pending: false,
autoplay: false,
loop: false,
webcam_options: { mirror: false, constraints: {} },
buttons: [] as (string | { value: string; id: number; icon: null })[]
};
beforeAll(() => {
window.HTMLMediaElement.prototype.play = vi.fn();
window.HTMLMediaElement.prototype.pause = vi.fn();
});
run_shared_prop_tests({
component: Video,
name: "Video",
base_props: {
...default_props
},
has_label: false,
has_validation_error: false
});
describe("Video", () => {
setupi18n();
afterEach(() => cleanup());
test("renders upload area when value is null", async () => {
const { getByLabelText } = await render(Video, {
...default_props,
sources: ["upload"],
value: null
});
expect(getByLabelText("video.drop_to_upload")).toBeVisible();
});
test("renders video player when value is set", async () => {
const { getByTestId } = await render(Video, {
...default_props,
label: "Test Video",
value: fake_value
});
const vid = getByTestId("Test Video-player") as HTMLVideoElement;
expect(vid).toBeTruthy();
expect(vid.src).toContain("video_sample.mp4");
});
});
describe("Props: sources", () => {
setupi18n();
afterEach(() => cleanup());
test("multiple sources renders source selection buttons", async () => {
const { getByTestId } = await render(Video, {
...default_props,
sources: ["upload", "webcam"]
});
expect(getByTestId("source-select")).toBeTruthy();
});
test("single upload source does not render source selection", async () => {
const { queryByTestId } = await render(Video, {
...default_props,
sources: ["upload"]
});
expect(queryByTestId("source-select")).toBeNull();
});
test("upload and webcam sources render corresponding buttons", async () => {
const { getByLabelText } = await render(Video, {
...default_props,
sources: ["upload", "webcam"]
});
expect(getByLabelText("Upload file")).toBeTruthy();
expect(getByLabelText("Capture from camera")).toBeTruthy();
});
});
describe("Props: interactive", () => {
setupi18n();
afterEach(() => cleanup());
test("interactive=true shows upload area when value is null", async () => {
const { getByLabelText } = await render(Video, {
...default_props,
interactive: true,
sources: ["upload"],
value: null
});
expect(getByLabelText("video.drop_to_upload")).toBeTruthy();
});
test("interactive=false renders the video without upload controls", async () => {
const { queryByLabelText } = await render(Video, {
...default_props,
interactive: false,
value: fake_value
});
expect(queryByLabelText("video.drop_to_upload")).toBeNull();
expect(queryByLabelText("Upload file")).toBeNull();
});
test("interactive=false with null value does not show upload area", async () => {
const { queryByLabelText } = await render(Video, {
...default_props,
interactive: false,
value: null
});
expect(queryByLabelText("video.drop_to_upload")).toBeNull();
});
});
describe("Events: change", () => {
setupi18n();
afterEach(() => cleanup());
test("setting value triggers change event", async () => {
const { listen, set_data } = await render(Video, {
...default_props,
value: null
});
const change = listen("change");
await set_data({ value: fake_value });
expect(change).toHaveBeenCalledTimes(1);
});
test("change event is not triggered on mount with a default value", async () => {
const { listen } = await render(Video, {
...default_props,
value: fake_value
});
const change = listen("change", { retrospective: true });
expect(change).not.toHaveBeenCalled();
});
test("changing value multiple times triggers change each time", async () => {
const { listen, set_data } = await render(Video, {
...default_props,
value: null
});
const change = listen("change");
const value_a = { ...fake_value, url: "https://example.com/a.mp4" };
const value_b = { ...fake_value, url: "https://example.com/b.mp4" };
await set_data({ value: value_a });
await set_data({ value: value_b });
expect(change).toHaveBeenCalledTimes(2);
});
test("setting value to null after a value triggers change", async () => {
const { listen, set_data } = await render(Video, {
...default_props,
value: fake_value
});
const change = listen("change");
await set_data({ value: null });
expect(change).toHaveBeenCalledTimes(1);
});
});
describe("Props: buttons (static mode)", () => {
setupi18n();
afterEach(() => cleanup());
test("buttons with download shows download button", async () => {
const { getAllByTestId } = await render(Video, {
...default_props,
interactive: false,
value: fake_value,
buttons: ["download"]
});
const downloadDiv = getAllByTestId("download-div")[0];
expect(downloadDiv).toBeTruthy();
const link = downloadDiv.querySelector("a");
expect(link?.getAttribute("href")).toContain("video_sample.mp4");
});
test("custom button renders and dispatches custom_button_click", async () => {
const { listen, getByLabelText } = await render(Video, {
...default_props,
interactive: false,
value: fake_value,
buttons: [{ value: "Analyze", id: 5, icon: null }]
});
const custom = listen("custom_button_click");
const btn = getByLabelText("Analyze");
await fireEvent.click(btn);
expect(custom).toHaveBeenCalledTimes(1);
expect(custom).toHaveBeenCalledWith({ id: 5 });
});
});
describe("Props: buttons (interactive mode)", () => {
setupi18n();
afterEach(() => cleanup());
test("clear button appears when video has a value", async () => {
const { getByLabelText } = await render(Video, {
...default_props,
interactive: true,
value: fake_value
});
const clearBtn = getByLabelText("common.clear");
expect(clearBtn).toBeTruthy();
});
test("clicking clear button removes the video and dispatches clear and input", async () => {
const { getByLabelText, listen } = await render(Video, {
...default_props,
interactive: true,
value: fake_value
});
const clear = listen("clear");
const input = listen("input");
const clearBtn = getByLabelText("common.clear");
await fireEvent.click(clearBtn);
expect(clear).toHaveBeenCalledTimes(1);
expect(input).toHaveBeenCalledTimes(1);
});
});
describe("get_data", () => {
setupi18n();
afterEach(() => cleanup());
test("get_data returns the current value", async () => {
const { get_data, set_data } = await render(Video, {
...default_props,
value: null
});
const initial = await get_data();
expect(initial.value).toBeNull();
await set_data({ value: fake_value });
const updated = await get_data();
expect(updated.value).toEqual(fake_value);
});
});
describe("Autoplay", () => {
setupi18n();
afterEach(() => cleanup());
test("autoplay calls media.play in static mode", async () => {
const { getByTestId } = await render(Video, {
...default_props,
interactive: false,
value: fake_value,
autoplay: true
});
const video = getByTestId("Video-player") as HTMLVideoElement;
const fn = vi.spyOn(video, "play").mockResolvedValue(undefined);
video.dispatchEvent(new Event("loadeddata"));
expect(fn).toHaveBeenCalledTimes(1);
});
test("autoplay calls media.play in interactive mode", async () => {
const { getByTestId } = await render(Video, {
...default_props,
interactive: true,
value: fake_value,
autoplay: true
});
const video = getByTestId("Video-player") as HTMLVideoElement;
const fn = vi.spyOn(video, "play").mockResolvedValue(undefined);
video.dispatchEvent(new Event("loadeddata"));
expect(fn).toHaveBeenCalled();
});
});
describe("Player controls", () => {
setupi18n();
afterEach(() => cleanup());
test("play/pause button is rendered when video has a value", async () => {
const { getByLabelText } = await render(Video, {
...default_props,
interactive: true,
value: fake_value
});
expect(getByLabelText("play-pause-replay-button")).toBeTruthy();
});
test("volume button is rendered", async () => {
const { getByLabelText } = await render(Video, {
...default_props,
interactive: true,
value: fake_value
});
expect(getByLabelText("Adjust volume")).toBeTruthy();
});
test("fullscreen button is rendered", async () => {
const { getByLabelText } = await render(Video, {
...default_props,
interactive: true,
value: fake_value
});
expect(getByLabelText("full-screen")).toBeTruthy();
});
test("trim button is rendered in interactive mode", async () => {
const { getByLabelText } = await render(Video, {
...default_props,
interactive: true,
value: fake_value
});
expect(getByLabelText("Trim video to selection")).toBeTruthy();
});
test("trim button is not rendered in static mode", async () => {
const { queryByLabelText } = await render(Video, {
...default_props,
interactive: false,
value: fake_value
});
expect(queryByLabelText("Trim video to selection")).toBeNull();
});
test("clicking trim button enters edit mode with Trim and Cancel buttons", async () => {
const { getByLabelText, getByText } = await render(Video, {
...default_props,
interactive: true,
value: fake_value
});
await fireEvent.click(getByLabelText("Trim video to selection"));
expect(getByText("Trim")).toBeTruthy();
expect(getByText("Cancel")).toBeTruthy();
});
test("clicking Cancel exits edit mode and restores trim button", async () => {
const { getByLabelText, getByText, queryByText } = await render(Video, {
...default_props,
interactive: true,
value: fake_value
});
await fireEvent.click(getByLabelText("Trim video to selection"));
expect(getByText("Cancel")).toBeTruthy();
await fireEvent.click(getByText("Cancel"));
expect(queryByText("Cancel")).toBeNull();
expect(getByLabelText("Trim video to selection")).toBeTruthy();
});
test("time display is rendered", async () => {
const { container } = await render(Video, {
...default_props,
interactive: true,
value: fake_value
});
const timeDisplay = container.querySelector(".time");
expect(timeDisplay).toBeTruthy();
});
});
const upload_props = {
...default_props,
sources: ["upload"] as "upload"[],
interactive: true,
value: null,
root: "https://example.com",
client: mock_client()
};
describe("Events: upload via file input", () => {
setupi18n();
afterEach(() => cleanup());
test("selecting a file triggers upload, change, and input events", async () => {
const { listen } = await render(Video, upload_props);
const upload = listen("upload");
const change = listen("change");
const input = listen("input");
await upload_file(TEST_MP4);
await waitFor(() => {
expect(upload).toHaveBeenCalledTimes(1);
});
expect(input).toHaveBeenCalledTimes(1);
expect(change).toHaveBeenCalledTimes(1);
});
test("drag and drop a file triggers upload, change, and input events", async () => {
const { listen } = await render(Video, upload_props);
const upload = listen("upload");
const change = listen("change");
const input = listen("input");
await drop_file(TEST_MP4, "[aria-label='video.drop_to_upload']");
await waitFor(() => {
expect(upload).toHaveBeenCalledTimes(1);
});
expect(input).toHaveBeenCalledTimes(1);
expect(change).toHaveBeenCalledTimes(1);
});
test("upload failure dispatches error event with the message", async () => {
const failing_upload = vi
.fn()
.mockRejectedValue(new Error("File too large"));
const { listen } = await render(Video, {
...upload_props,
client: {
upload: failing_upload,
stream: async () => ({ onmessage: null, close: () => {} })
}
});
const error = listen("error");
await upload_file(TEST_MP4);
await waitFor(() => {
expect(failing_upload).toHaveBeenCalled();
});
await waitFor(() => {
expect(error).toHaveBeenCalledTimes(1);
});
expect(error).toHaveBeenCalledWith("File too large");
});
});