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
785 lines
19 KiB
TypeScript
785 lines
19 KiB
TypeScript
import { test, describe, afterEach, expect, vi } from "vitest";
|
|
import {
|
|
cleanup,
|
|
render,
|
|
fireEvent,
|
|
waitFor,
|
|
upload_file,
|
|
drop_file,
|
|
mock_client,
|
|
download_file,
|
|
TEST_JPG,
|
|
TEST_PNG
|
|
} from "@self/tootils/render";
|
|
import { run_shared_prop_tests } from "@self/tootils/shared-prop-tests";
|
|
|
|
import Image from "./Index.svelte";
|
|
import { get_coordinates_of_clicked_image } from "./shared/utils";
|
|
|
|
const fake_value = {
|
|
path: "test.png",
|
|
url: "https://example.com/test.png",
|
|
orig_name: "test.png",
|
|
size: 1024,
|
|
mime_type: "image/png",
|
|
is_stream: false
|
|
};
|
|
|
|
const default_props = {
|
|
sources: ["upload", "webcam", "clipboard"] as (
|
|
| "upload"
|
|
| "webcam"
|
|
| "clipboard"
|
|
)[],
|
|
value: null as any,
|
|
label: "Image",
|
|
show_label: true,
|
|
interactive: true,
|
|
_selectable: false,
|
|
height: 300,
|
|
width: 300,
|
|
streaming: false,
|
|
stream_every: 1,
|
|
pending: false,
|
|
input_ready: true,
|
|
placeholder: "",
|
|
buttons: [] as (string | { value: string; id: number; icon: null })[],
|
|
webcam_options: { mirror: false, constraints: {} },
|
|
watermark: null
|
|
};
|
|
|
|
run_shared_prop_tests({
|
|
component: Image,
|
|
name: "Image",
|
|
base_props: {
|
|
...default_props
|
|
},
|
|
has_label: false,
|
|
has_validation_error: true
|
|
});
|
|
|
|
describe("Image", () => {
|
|
afterEach(() => cleanup());
|
|
|
|
test("renders with null value showing upload area", async () => {
|
|
const { getByLabelText } = await render(Image, {
|
|
...default_props,
|
|
value: null
|
|
});
|
|
|
|
expect(getByLabelText("image.drop_to_upload")).toBeVisible();
|
|
});
|
|
|
|
test("renders image when value is set", async () => {
|
|
const { container } = await render(Image, {
|
|
...default_props,
|
|
value: fake_value
|
|
});
|
|
|
|
const img = container.querySelector("img");
|
|
expect(img).toBeTruthy();
|
|
expect(img?.getAttribute("src")).toBe("https://example.com/test.png");
|
|
});
|
|
});
|
|
|
|
describe("Props: label", () => {
|
|
afterEach(() => cleanup());
|
|
|
|
test("label text is rendered", async () => {
|
|
const result = await render(Image, {
|
|
...default_props,
|
|
label: "My Custom Label",
|
|
show_label: true
|
|
});
|
|
const el = result.getByText("My Custom Label");
|
|
expect(el).toBeTruthy();
|
|
});
|
|
|
|
test("show_label: true makes the label visible", async () => {
|
|
const result = await render(Image, {
|
|
...default_props,
|
|
label: "Visible Label",
|
|
show_label: true
|
|
});
|
|
const el = result.getByText("Visible Label");
|
|
expect(el).toBeVisible();
|
|
});
|
|
|
|
test("show_label: false hides the label visually but keeps it in the DOM", async () => {
|
|
const result = await render(Image, {
|
|
...default_props,
|
|
label: "Hidden Label",
|
|
show_label: false
|
|
});
|
|
const el = result.getByText("Hidden Label");
|
|
expect(el).not.toBeVisible();
|
|
});
|
|
});
|
|
|
|
describe("Props: sources", () => {
|
|
afterEach(() => cleanup());
|
|
|
|
test("multiple sources renders source selection buttons", async () => {
|
|
const { getByTestId } = await render(Image, {
|
|
...default_props,
|
|
sources: ["upload", "webcam", "clipboard"]
|
|
});
|
|
|
|
const sourceSelect = getByTestId("source-select");
|
|
expect(sourceSelect).toBeTruthy();
|
|
});
|
|
|
|
test("single upload source does not render source selection", async () => {
|
|
const { queryByTestId, getByLabelText } = await render(Image, {
|
|
...default_props,
|
|
sources: ["upload"]
|
|
});
|
|
expect(getByLabelText("image.drop_to_upload")).toBeVisible();
|
|
const sourceSelect = queryByTestId("source-select");
|
|
expect(sourceSelect).toBeNull();
|
|
});
|
|
|
|
test("single clipboard source does render source selection", async () => {
|
|
const { queryByTestId, getByLabelText } = await render(Image, {
|
|
...default_props,
|
|
sources: ["clipboard"]
|
|
});
|
|
expect(getByLabelText("Paste from clipboard")).toBeTruthy();
|
|
const sourceSelect = queryByTestId("source-select");
|
|
expect(sourceSelect).not.toBeNull();
|
|
});
|
|
|
|
test("clipboard and upload sources render paste and upload buttons", async () => {
|
|
const { getByLabelText } = await render(Image, {
|
|
...default_props,
|
|
sources: ["upload", "clipboard"]
|
|
});
|
|
|
|
expect(getByLabelText("Upload file")).toBeTruthy();
|
|
expect(getByLabelText("Paste from clipboard")).toBeTruthy();
|
|
});
|
|
|
|
test("upload and webcam sources render corresponding buttons", async () => {
|
|
const { getByLabelText } = await render(Image, {
|
|
...default_props,
|
|
sources: ["upload", "webcam"]
|
|
});
|
|
|
|
expect(getByLabelText("Upload file")).toBeTruthy();
|
|
expect(getByLabelText("Capture from camera")).toBeTruthy();
|
|
});
|
|
|
|
test("clicking webcam source button hides the upload area", async () => {
|
|
const { getByLabelText } = await render(Image, {
|
|
...default_props,
|
|
sources: ["upload", "webcam"]
|
|
});
|
|
|
|
expect(getByLabelText("image.drop_to_upload")).toBeVisible();
|
|
|
|
await fireEvent.click(getByLabelText("Capture from camera"));
|
|
|
|
// Re-query after click to avoid stale references from potential rerenders
|
|
expect(getByLabelText("image.drop_to_upload")).not.toBeVisible();
|
|
});
|
|
|
|
test("clicking upload source button shows the upload area again", async () => {
|
|
const { getByLabelText } = await render(Image, {
|
|
...default_props,
|
|
sources: ["upload", "webcam"]
|
|
});
|
|
|
|
await fireEvent.click(getByLabelText("Capture from camera"));
|
|
expect(getByLabelText("image.drop_to_upload")).not.toBeVisible();
|
|
|
|
await fireEvent.click(getByLabelText("Upload file"));
|
|
expect(getByLabelText("image.drop_to_upload")).toBeVisible();
|
|
});
|
|
});
|
|
|
|
describe("Props: interactive", () => {
|
|
afterEach(() => cleanup());
|
|
|
|
test("interactive=true shows an upload area when value is null", async () => {
|
|
const { getByLabelText } = await render(Image, {
|
|
...default_props,
|
|
interactive: true,
|
|
value: null
|
|
});
|
|
|
|
expect(getByLabelText("image.drop_to_upload")).toBeTruthy();
|
|
});
|
|
|
|
test("interactive=false renders the image without upload controls", async () => {
|
|
const { container, queryByLabelText } = await render(Image, {
|
|
...default_props,
|
|
interactive: false,
|
|
value: fake_value,
|
|
buttons: ["fullscreen"]
|
|
});
|
|
|
|
const img = container.querySelector("img");
|
|
expect(img).toBeTruthy();
|
|
// No upload area or source selection in static mode
|
|
expect(queryByLabelText("image.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(Image, {
|
|
...default_props,
|
|
interactive: false,
|
|
value: null
|
|
});
|
|
|
|
expect(queryByLabelText("image.drop_to_upload")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("Events: change", () => {
|
|
afterEach(() => cleanup());
|
|
|
|
test("setting value triggers change event", async () => {
|
|
const { listen, set_data } = await render(Image, {
|
|
...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(Image, {
|
|
...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(Image, {
|
|
...default_props,
|
|
value: null
|
|
});
|
|
|
|
const change = listen("change");
|
|
|
|
const value_a = { ...fake_value, url: "https://example.com/a.png" };
|
|
const value_b = { ...fake_value, url: "https://example.com/b.png" };
|
|
|
|
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(Image, {
|
|
...default_props,
|
|
value: fake_value
|
|
});
|
|
|
|
const change = listen("change");
|
|
|
|
await set_data({ value: null });
|
|
|
|
expect(change).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe("Props: buttons (static mode)", () => {
|
|
afterEach(() => cleanup());
|
|
|
|
test("buttons with download shows download link", async () => {
|
|
const { container } = await render(Image, {
|
|
...default_props,
|
|
interactive: false,
|
|
value: {
|
|
...TEST_JPG,
|
|
is_stream: false
|
|
},
|
|
buttons: ["download"]
|
|
});
|
|
|
|
const downloadLink = container.querySelector("a.download-link");
|
|
expect(downloadLink).toBeTruthy();
|
|
|
|
const { suggested_filename } = await download_file("a.download-link");
|
|
expect(suggested_filename).toBe("cheetah1.jpg");
|
|
});
|
|
|
|
test("buttons with fullscreen shows fullscreen button", async () => {
|
|
const { getByLabelText } = await render(Image, {
|
|
...default_props,
|
|
interactive: false,
|
|
value: fake_value,
|
|
buttons: ["fullscreen"]
|
|
});
|
|
|
|
expect(getByLabelText("Fullscreen")).toBeTruthy();
|
|
});
|
|
|
|
test("clicking the fullscreen button toggles fullscreen in interactive mode", async () => {
|
|
const { getByLabelText } = await render(Image, {
|
|
...default_props,
|
|
interactive: true,
|
|
value: fake_value,
|
|
buttons: ["fullscreen"]
|
|
});
|
|
|
|
await fireEvent.click(getByLabelText("Fullscreen"));
|
|
await waitFor(() => {
|
|
expect(getByLabelText("Exit fullscreen mode")).toBeVisible();
|
|
});
|
|
|
|
await fireEvent.click(getByLabelText("Exit fullscreen mode"));
|
|
await waitFor(() => {
|
|
expect(getByLabelText("Fullscreen")).toBeVisible();
|
|
});
|
|
});
|
|
|
|
test("empty buttons array shows no action buttons", async () => {
|
|
const { queryByLabelText } = await render(Image, {
|
|
...default_props,
|
|
interactive: false,
|
|
value: fake_value,
|
|
buttons: []
|
|
});
|
|
|
|
expect(queryByLabelText("Fullscreen")).toBeNull();
|
|
expect(queryByLabelText("common.download")).toBeNull();
|
|
});
|
|
|
|
test("custom button renders and dispatches custom_button_click", async () => {
|
|
const { listen, getByLabelText } = await render(Image, {
|
|
...default_props,
|
|
interactive: false,
|
|
value: fake_value,
|
|
buttons: [{ value: "Analyze", id: 7, icon: null }]
|
|
});
|
|
|
|
const custom = listen("custom_button_click");
|
|
const btn = getByLabelText("Analyze");
|
|
|
|
await fireEvent.click(btn);
|
|
|
|
expect(custom).toHaveBeenCalledTimes(1);
|
|
expect(custom).toHaveBeenCalledWith({ id: 7 });
|
|
});
|
|
});
|
|
|
|
describe("Props: buttons (interactive mode)", () => {
|
|
afterEach(() => cleanup());
|
|
|
|
test("clear button appears when image has a value", async () => {
|
|
const { getByLabelText } = await render(Image, {
|
|
...default_props,
|
|
interactive: true,
|
|
value: fake_value
|
|
});
|
|
|
|
const clearBtn = getByLabelText("Remove Image");
|
|
expect(clearBtn).toBeTruthy();
|
|
});
|
|
|
|
test("clear button is not present when there is no image value", async () => {
|
|
const { queryByLabelText, getByLabelText } = await render(Image, {
|
|
...default_props,
|
|
interactive: true,
|
|
value: null
|
|
});
|
|
|
|
// Smoke test: component rendered
|
|
expect(getByLabelText("image.drop_to_upload")).toBeTruthy();
|
|
expect(queryByLabelText("Remove Image")).toBeNull();
|
|
});
|
|
|
|
test("clicking clear button removes the image and dispatches clear and input", async () => {
|
|
const { getByLabelText, listen } = await render(Image, {
|
|
...default_props,
|
|
interactive: true,
|
|
value: fake_value
|
|
});
|
|
|
|
const clear = listen("clear");
|
|
const input = listen("input");
|
|
const clearBtn = getByLabelText("Remove Image");
|
|
|
|
await fireEvent.click(clearBtn);
|
|
|
|
expect(clear).toHaveBeenCalledTimes(1);
|
|
expect(input).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe("get_data", () => {
|
|
afterEach(() => cleanup());
|
|
|
|
test("get_data returns the current value", async () => {
|
|
const { get_data, set_data } = await render(Image, {
|
|
...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("Selectable", () => {
|
|
afterEach(() => cleanup());
|
|
|
|
test("selectable mode shows crosshair cursor on the image", async () => {
|
|
const { container } = await render(Image, {
|
|
...default_props,
|
|
interactive: false,
|
|
value: fake_value,
|
|
_selectable: true,
|
|
buttons: []
|
|
});
|
|
|
|
const frame = container.querySelector(".selectable");
|
|
expect(frame).toBeTruthy();
|
|
});
|
|
|
|
test("non-selectable mode does not show crosshair cursor", async () => {
|
|
const { container } = await render(Image, {
|
|
...default_props,
|
|
interactive: false,
|
|
value: fake_value,
|
|
_selectable: false,
|
|
buttons: []
|
|
});
|
|
|
|
const frame = container.querySelector(".selectable");
|
|
expect(frame).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("get_coordinates_of_clicked_image", () => {
|
|
function make_mock_event(
|
|
clientX: number,
|
|
clientY: number,
|
|
imgProps: { naturalWidth: number; naturalHeight: number },
|
|
rect: { left: number; top: number; width: number; height: number }
|
|
): MouseEvent {
|
|
const imgEl = document.createElement("img");
|
|
Object.defineProperty(imgEl, "naturalWidth", {
|
|
value: imgProps.naturalWidth
|
|
});
|
|
Object.defineProperty(imgEl, "naturalHeight", {
|
|
value: imgProps.naturalHeight
|
|
});
|
|
imgEl.getBoundingClientRect = () => ({
|
|
left: rect.left,
|
|
top: rect.top,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
right: rect.left + rect.width,
|
|
bottom: rect.top + rect.height,
|
|
x: rect.left,
|
|
y: rect.top,
|
|
toJSON: () => {}
|
|
});
|
|
|
|
const container = document.createElement("div");
|
|
container.appendChild(imgEl);
|
|
|
|
return {
|
|
currentTarget: container,
|
|
clientX,
|
|
clientY
|
|
} as unknown as MouseEvent;
|
|
}
|
|
|
|
test("returns correct coordinates for a 1:1 scale image", () => {
|
|
const evt = make_mock_event(
|
|
50,
|
|
50,
|
|
{
|
|
naturalWidth: 100,
|
|
naturalHeight: 100
|
|
},
|
|
{
|
|
left: 0,
|
|
top: 0,
|
|
width: 100,
|
|
height: 100
|
|
}
|
|
);
|
|
|
|
const result = get_coordinates_of_clicked_image(evt);
|
|
expect(result).toEqual([50, 50]);
|
|
});
|
|
|
|
test("returns correct coordinates when image is scaled down", () => {
|
|
// 200x200 natural, displayed at 100x100, click at (25, 25) in viewport
|
|
const evt = make_mock_event(
|
|
25,
|
|
25,
|
|
{
|
|
naturalWidth: 200,
|
|
naturalHeight: 200
|
|
},
|
|
{
|
|
left: 0,
|
|
top: 0,
|
|
width: 100,
|
|
height: 100
|
|
}
|
|
);
|
|
|
|
const result = get_coordinates_of_clicked_image(evt);
|
|
expect(result).toEqual([50, 50]);
|
|
});
|
|
|
|
test("accounts for container offset", () => {
|
|
const evt = make_mock_event(
|
|
60,
|
|
70,
|
|
{
|
|
naturalWidth: 100,
|
|
naturalHeight: 100
|
|
},
|
|
{
|
|
left: 10,
|
|
top: 20,
|
|
width: 100,
|
|
height: 100
|
|
}
|
|
);
|
|
|
|
const result = get_coordinates_of_clicked_image(evt);
|
|
expect(result).toEqual([50, 50]);
|
|
});
|
|
|
|
test("returns null when click is outside image bounds", () => {
|
|
// Click at (-5, 50) relative to image → x = -5 which is < 0
|
|
const evt = make_mock_event(
|
|
-5,
|
|
50,
|
|
{
|
|
naturalWidth: 100,
|
|
naturalHeight: 100
|
|
},
|
|
{
|
|
left: 0,
|
|
top: 0,
|
|
width: 100,
|
|
height: 100
|
|
}
|
|
);
|
|
|
|
const result = get_coordinates_of_clicked_image(evt);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
test("returns null when click is beyond the right edge", () => {
|
|
const evt = make_mock_event(
|
|
105,
|
|
50,
|
|
{
|
|
naturalWidth: 100,
|
|
naturalHeight: 100
|
|
},
|
|
{
|
|
left: 0,
|
|
top: 0,
|
|
width: 100,
|
|
height: 100
|
|
}
|
|
);
|
|
|
|
const result = get_coordinates_of_clicked_image(evt);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
test("handles landscape image with letterboxing (xScale > yScale)", () => {
|
|
// 400x200 natural image displayed in 200x200 container
|
|
// xScale = 400/200 = 2, yScale = 200/200 = 1
|
|
// xScale > yScale, so displayed_height = 200/2 = 100, y_offset = 50
|
|
// Click at (100, 100): x = (100-0)*2 = 200, y = (100-0-50)*2 = 100
|
|
const evt = make_mock_event(
|
|
100,
|
|
100,
|
|
{
|
|
naturalWidth: 400,
|
|
naturalHeight: 200
|
|
},
|
|
{
|
|
left: 0,
|
|
top: 0,
|
|
width: 200,
|
|
height: 200
|
|
}
|
|
);
|
|
|
|
const result = get_coordinates_of_clicked_image(evt);
|
|
expect(result).toEqual([200, 100]);
|
|
});
|
|
|
|
test("handles portrait image with pillarboxing (yScale > xScale)", () => {
|
|
// 200x400 natural image displayed in 200x200 container
|
|
// xScale = 200/200 = 1, yScale = 400/200 = 2
|
|
// yScale > xScale, so displayed_width = 200/2 = 100, x_offset = 50
|
|
// Click at (100, 100): x = (100-0-50)*2 = 100, y = (100-0)*2 = 200
|
|
const evt = make_mock_event(
|
|
100,
|
|
100,
|
|
{
|
|
naturalWidth: 200,
|
|
naturalHeight: 400
|
|
},
|
|
{
|
|
left: 0,
|
|
top: 0,
|
|
width: 200,
|
|
height: 200
|
|
}
|
|
);
|
|
|
|
const result = get_coordinates_of_clicked_image(evt);
|
|
expect(result).toEqual([100, 200]);
|
|
});
|
|
|
|
test("handles image shown at natural size with empty space on both axes", () => {
|
|
// `object-fit: scale-down` never upscales: a 100x100 natural image in
|
|
// a 400x300 box is drawn at natural size, centered at offset (150, 100).
|
|
// Click at (150, 100) = top-left corner of the drawn image.
|
|
const evt = make_mock_event(
|
|
150,
|
|
100,
|
|
{
|
|
naturalWidth: 100,
|
|
naturalHeight: 100
|
|
},
|
|
{
|
|
left: 0,
|
|
top: 0,
|
|
width: 400,
|
|
height: 300
|
|
}
|
|
);
|
|
|
|
const result = get_coordinates_of_clicked_image(evt);
|
|
expect(result).toEqual([0, 0]);
|
|
});
|
|
|
|
test("returns null when click is in the empty space around a natural-size image", () => {
|
|
// 100x100 natural image in a 1920x1080 box: drawn at natural size,
|
|
// centered at offset (910, 490). A click at (500, 540) is inside the
|
|
// box but left of the drawn image, so no image pixel is under it.
|
|
const evt = make_mock_event(
|
|
500,
|
|
540,
|
|
{
|
|
naturalWidth: 100,
|
|
naturalHeight: 100
|
|
},
|
|
{
|
|
left: 0,
|
|
top: 0,
|
|
width: 1920,
|
|
height: 1080
|
|
}
|
|
);
|
|
|
|
const result = get_coordinates_of_clicked_image(evt);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
test("returns [NaN, NaN] when currentTarget is not an Element", () => {
|
|
const evt = {
|
|
currentTarget: {},
|
|
clientX: 50,
|
|
clientY: 50
|
|
} as unknown as MouseEvent;
|
|
|
|
const result = get_coordinates_of_clicked_image(evt);
|
|
expect(result).toEqual([NaN, NaN]);
|
|
});
|
|
});
|
|
|
|
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", () => {
|
|
afterEach(() => cleanup());
|
|
|
|
test("selecting a file triggers upload, change, and input events", async () => {
|
|
const { listen } = await render(Image, upload_props);
|
|
|
|
const upload = listen("upload");
|
|
const change = listen("change");
|
|
const input = listen("input");
|
|
|
|
await upload_file(TEST_JPG);
|
|
|
|
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(Image, upload_props);
|
|
|
|
const upload = listen("upload");
|
|
const change = listen("change");
|
|
const input = listen("input");
|
|
|
|
await drop_file(TEST_PNG, "[aria-label='image.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(Image, {
|
|
...upload_props,
|
|
client: {
|
|
upload: failing_upload,
|
|
stream: async () => ({ onmessage: null, close: () => {} })
|
|
}
|
|
});
|
|
|
|
const error = listen("error");
|
|
|
|
await upload_file(TEST_JPG);
|
|
|
|
await waitFor(() => {
|
|
expect(failing_upload).toHaveBeenCalled();
|
|
});
|
|
|
|
await waitFor(() => {
|
|
expect(error).toHaveBeenCalledTimes(1);
|
|
});
|
|
expect(error).toHaveBeenCalledWith("File too large");
|
|
});
|
|
});
|