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
555 lines
17 KiB
TypeScript
555 lines
17 KiB
TypeScript
import { test, describe, afterEach, expect } from "vitest";
|
|
import { cleanup, render, fireEvent } from "@self/tootils/render";
|
|
import { run_shared_prop_tests } from "@self/tootils/shared-prop-tests";
|
|
|
|
import Label from "./Index.svelte";
|
|
|
|
const default_props = {
|
|
value: {
|
|
label: "Cat",
|
|
confidences: [
|
|
{ label: "cat", confidence: 0.9 },
|
|
{ label: "dog", confidence: 0.08 },
|
|
{ label: "lion", confidence: 0.02 }
|
|
]
|
|
},
|
|
show_heading: true,
|
|
_selectable: false,
|
|
buttons: null
|
|
};
|
|
|
|
// has_label: false because the Label component removes BlockLabel from the DOM
|
|
// when show_label=false (using {#if show_label}) rather than keeping it with
|
|
// sr-only. The custom label tests below cover label/show_label behaviour.
|
|
run_shared_prop_tests({
|
|
component: Label,
|
|
name: "Label",
|
|
base_props: {
|
|
value: { label: "Cat" },
|
|
show_heading: true,
|
|
_selectable: false,
|
|
buttons: null
|
|
},
|
|
has_label: false
|
|
});
|
|
|
|
describe("Label: label prop", () => {
|
|
afterEach(() => cleanup());
|
|
|
|
test("label text is rendered in the block header when show_label=true", async () => {
|
|
const { getByText } = await render(Label, {
|
|
...default_props,
|
|
label: "My Output",
|
|
show_label: true
|
|
});
|
|
expect(getByText("My Output")).toBeInTheDocument();
|
|
});
|
|
|
|
test("show_label=true makes the block label visible", async () => {
|
|
const { getByText } = await render(Label, {
|
|
...default_props,
|
|
label: "Visible Label",
|
|
show_label: true
|
|
});
|
|
expect(getByText("Visible Label")).toBeVisible();
|
|
});
|
|
|
|
test("show_label=false removes the block label from the DOM", async () => {
|
|
const { queryByText } = await render(Label, {
|
|
...default_props,
|
|
label: "Hidden Label",
|
|
show_label: false
|
|
});
|
|
// Label uses conditional rendering ({#if show_label}) so the element
|
|
// is absent from the DOM entirely rather than hidden with sr-only.
|
|
expect(queryByText("Hidden Label")).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("Label — rendering", () => {
|
|
afterEach(() => cleanup());
|
|
|
|
test("renders label text in heading", async () => {
|
|
const { getByTestId } = await render(Label, {
|
|
value: { label: "Cat" },
|
|
show_heading: true,
|
|
_selectable: false,
|
|
buttons: null
|
|
});
|
|
expect(getByTestId("label-output-value")).toHaveTextContent("Cat");
|
|
});
|
|
|
|
test("renders one confidence bar for each item in confidences", async () => {
|
|
const { getByTestId } = await render(Label, default_props);
|
|
expect(getByTestId("cat-confidence-set")).toBeInTheDocument();
|
|
expect(getByTestId("dog-confidence-set")).toBeInTheDocument();
|
|
expect(getByTestId("lion-confidence-set")).toBeInTheDocument();
|
|
});
|
|
|
|
test("shows empty state when value.label is null", async () => {
|
|
const { queryByTestId } = await render(Label, {
|
|
...default_props,
|
|
value: { label: null }
|
|
});
|
|
expect(queryByTestId("label-output-value")).not.toBeInTheDocument();
|
|
});
|
|
|
|
test("shows empty state when value has no label property", async () => {
|
|
const { queryByTestId } = await render(Label, {
|
|
...default_props,
|
|
value: {}
|
|
});
|
|
expect(queryByTestId("label-output-value")).not.toBeInTheDocument();
|
|
});
|
|
|
|
test("heading text matches value.label", async () => {
|
|
const { getByTestId } = await render(Label, {
|
|
...default_props,
|
|
value: { label: "Golden Retriever" }
|
|
});
|
|
expect(getByTestId("label-output-value")).toHaveTextContent(
|
|
"Golden Retriever"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Props: value — confidence bars", () => {
|
|
afterEach(() => cleanup());
|
|
|
|
test("confidence percentage is rounded to nearest integer (0.855 → 86%)", async () => {
|
|
const { getByText } = await render(Label, {
|
|
...default_props,
|
|
value: {
|
|
label: "Cat",
|
|
confidences: [{ label: "cat", confidence: 0.855 }]
|
|
}
|
|
});
|
|
expect(getByText("86%")).toBeInTheDocument();
|
|
});
|
|
|
|
test("confidence of 0 displays as 0% with correct aria-valuenow", async () => {
|
|
const { getByText, getByRole } = await render(Label, {
|
|
...default_props,
|
|
value: {
|
|
label: "cat",
|
|
confidences: [{ label: "cat", confidence: 0 }]
|
|
}
|
|
});
|
|
expect(getByText("0%")).toBeInTheDocument();
|
|
expect(getByRole("meter", { name: "cat" })).toHaveAttribute(
|
|
"aria-valuenow",
|
|
"0"
|
|
);
|
|
});
|
|
|
|
test("confidence of 1 displays as 100% with correct aria-valuenow", async () => {
|
|
const { getByText, getByRole } = await render(Label, {
|
|
...default_props,
|
|
value: {
|
|
label: "cat",
|
|
confidences: [{ label: "cat", confidence: 1 }]
|
|
}
|
|
});
|
|
expect(getByText("100%")).toBeInTheDocument();
|
|
expect(getByRole("meter", { name: "cat" })).toHaveAttribute(
|
|
"aria-valuenow",
|
|
"100"
|
|
);
|
|
});
|
|
|
|
test("confidence label text is displayed inside each bar", async () => {
|
|
// Use a heading label distinct from the confidence labels to avoid
|
|
// getByText finding duplicate matches (heading + bar).
|
|
const { getByText } = await render(Label, {
|
|
...default_props,
|
|
value: {
|
|
label: "TopResult",
|
|
confidences: [
|
|
{ label: "dog", confidence: 0.7 },
|
|
{ label: "cat", confidence: 0.3 }
|
|
]
|
|
}
|
|
});
|
|
expect(getByText("dog")).toBeInTheDocument();
|
|
expect(getByText("cat")).toBeInTheDocument();
|
|
});
|
|
|
|
test("empty confidences array renders no confidence bars", async () => {
|
|
const { queryAllByRole } = await render(Label, {
|
|
...default_props,
|
|
value: { label: "Cat", confidences: [] }
|
|
});
|
|
expect(queryAllByRole("button")).toHaveLength(0);
|
|
});
|
|
|
|
test("single confidence item renders exactly one bar", async () => {
|
|
const { getByTestId, queryAllByRole } = await render(Label, {
|
|
...default_props,
|
|
value: {
|
|
label: "cat",
|
|
confidences: [{ label: "cat", confidence: 0.9 }]
|
|
}
|
|
});
|
|
expect(getByTestId("cat-confidence-set")).toBeInTheDocument();
|
|
expect(queryAllByRole("button")).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe("Props: show_heading", () => {
|
|
afterEach(() => cleanup());
|
|
|
|
test("show_heading=true with confidences: heading is visible", async () => {
|
|
const { getByTestId } = await render(Label, default_props);
|
|
expect(getByTestId("label-output-value")).toBeVisible();
|
|
});
|
|
|
|
test("show_heading=false with confidences: heading is not in the DOM", async () => {
|
|
const { queryByTestId } = await render(Label, {
|
|
...default_props,
|
|
show_heading: false
|
|
});
|
|
expect(queryByTestId("label-output-value")).not.toBeInTheDocument();
|
|
});
|
|
|
|
test("show_heading=false without confidences: heading still renders (fallback to !confidences)", async () => {
|
|
// The template condition is `show_heading || !value.confidences`.
|
|
// When show_heading=false but there are no confidences, the heading still shows.
|
|
const { getByTestId } = await render(Label, {
|
|
...default_props,
|
|
show_heading: false,
|
|
value: { label: "Cat" }
|
|
});
|
|
expect(getByTestId("label-output-value")).toBeVisible();
|
|
});
|
|
|
|
test("show_heading=true without confidences: heading is visible", async () => {
|
|
const { getByTestId } = await render(Label, {
|
|
...default_props,
|
|
show_heading: true,
|
|
value: { label: "Cat" }
|
|
});
|
|
expect(getByTestId("label-output-value")).toBeVisible();
|
|
});
|
|
});
|
|
|
|
describe("Props: _selectable", () => {
|
|
afterEach(() => cleanup());
|
|
|
|
test("clicking confidence bar dispatches select with correct index and value", async () => {
|
|
const { listen, getByTestId } = await render(Label, {
|
|
...default_props,
|
|
_selectable: true
|
|
});
|
|
const select = listen("select");
|
|
await fireEvent.click(getByTestId("cat-confidence-set"));
|
|
expect(select).toHaveBeenCalledTimes(1);
|
|
expect(select).toHaveBeenCalledWith({ index: 0, value: "cat" });
|
|
});
|
|
|
|
test("clicking second confidence bar dispatches select with index 1", async () => {
|
|
const { listen, getByTestId } = await render(Label, {
|
|
...default_props,
|
|
_selectable: true
|
|
});
|
|
const select = listen("select");
|
|
await fireEvent.click(getByTestId("dog-confidence-set"));
|
|
expect(select).toHaveBeenCalledWith({ index: 1, value: "dog" });
|
|
});
|
|
|
|
test("clicking third confidence bar dispatches select with index 2", async () => {
|
|
const { listen, getByTestId } = await render(Label, {
|
|
...default_props,
|
|
_selectable: true
|
|
});
|
|
const select = listen("select");
|
|
await fireEvent.click(getByTestId("lion-confidence-set"));
|
|
expect(select).toHaveBeenCalledWith({ index: 2, value: "lion" });
|
|
});
|
|
|
|
test("_selectable=false: select event still fires on click (onselect is always bound; _selectable controls visual styling only)", async () => {
|
|
const { listen, getByTestId } = await render(Label, {
|
|
...default_props,
|
|
_selectable: false
|
|
});
|
|
const select = listen("select");
|
|
await fireEvent.click(getByTestId("cat-confidence-set"));
|
|
expect(select).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test.todo(
|
|
"VISUAL: _selectable=true adds 'selectable' CSS class to confidence bars, enabling hover cursor and colour changes — needs Playwright visual regression screenshot comparison"
|
|
);
|
|
});
|
|
|
|
describe("Props: color", () => {
|
|
test.todo(
|
|
"VISUAL: color='red' applies a red background colour to the label heading element — needs Playwright visual regression screenshot comparison"
|
|
);
|
|
});
|
|
|
|
describe("Props: buttons", () => {
|
|
afterEach(() => cleanup());
|
|
|
|
test("buttons=null renders no custom buttons", async () => {
|
|
const { queryByLabelText } = await render(Label, {
|
|
...default_props,
|
|
buttons: null
|
|
});
|
|
expect(queryByLabelText("Analyze")).not.toBeInTheDocument();
|
|
});
|
|
|
|
test("custom button is visible when show_label=true", async () => {
|
|
const { getByLabelText } = await render(Label, {
|
|
...default_props,
|
|
show_label: true,
|
|
buttons: [{ value: "Analyze", id: 5, icon: null }]
|
|
});
|
|
expect(getByLabelText("Analyze")).toBeInTheDocument();
|
|
});
|
|
|
|
test("clicking custom button dispatches custom_button_click", async () => {
|
|
const { listen, getByLabelText } = await render(Label, {
|
|
...default_props,
|
|
show_label: true,
|
|
buttons: [{ value: "Analyze", id: 5, icon: null }]
|
|
});
|
|
const click = listen("custom_button_click");
|
|
await fireEvent.click(getByLabelText("Analyze"));
|
|
expect(click).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test("custom_button_click payload contains the button's id", async () => {
|
|
const { listen, getByLabelText } = await render(Label, {
|
|
...default_props,
|
|
show_label: true,
|
|
buttons: [{ value: "Analyze", id: 5, icon: null }]
|
|
});
|
|
const click = listen("custom_button_click");
|
|
await fireEvent.click(getByLabelText("Analyze"));
|
|
expect(click).toHaveBeenCalledWith({ id: 5 });
|
|
});
|
|
|
|
test("custom buttons are not shown when show_label=false", async () => {
|
|
const { queryByLabelText } = await render(Label, {
|
|
...default_props,
|
|
show_label: false,
|
|
buttons: [{ value: "Analyze", id: 5, icon: null }]
|
|
});
|
|
expect(queryByLabelText("Analyze")).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("Events", () => {
|
|
afterEach(() => cleanup());
|
|
|
|
test("change: fired when value changes via set_data", async () => {
|
|
const { listen, set_data } = await render(Label, default_props);
|
|
const change = listen("change");
|
|
await set_data({ value: { label: "Dog" } });
|
|
expect(change).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test("change: not fired on initial mount", async () => {
|
|
const { listen } = await render(Label, default_props);
|
|
const change = listen("change", { retrospective: true });
|
|
expect(change).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("change: fired again on a subsequent value update", async () => {
|
|
const { listen, set_data } = await render(Label, default_props);
|
|
const change = listen("change");
|
|
await set_data({ value: { label: "Dog" } });
|
|
await set_data({ value: { label: "Bird" } });
|
|
expect(change).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
test("select: dispatched with correct payload when confidence bar is clicked", async () => {
|
|
const { listen, getByTestId } = await render(Label, {
|
|
...default_props,
|
|
_selectable: true
|
|
});
|
|
const select = listen("select");
|
|
await fireEvent.click(getByTestId("cat-confidence-set"));
|
|
expect(select).toHaveBeenCalledTimes(1);
|
|
expect(select).toHaveBeenCalledWith({ index: 0, value: "cat" });
|
|
});
|
|
});
|
|
|
|
describe("get_data / set_data", () => {
|
|
afterEach(() => cleanup());
|
|
|
|
test("set_data updates the heading text", async () => {
|
|
const { set_data, getByTestId } = await render(Label, default_props);
|
|
await set_data({ value: { label: "Elephant" } });
|
|
expect(getByTestId("label-output-value")).toHaveTextContent("Elephant");
|
|
});
|
|
|
|
test("set_data with confidences causes confidence bars to appear", async () => {
|
|
const { set_data, getByTestId } = await render(Label, {
|
|
...default_props,
|
|
value: { label: "Cat" }
|
|
});
|
|
await set_data({
|
|
value: {
|
|
label: "cat",
|
|
confidences: [{ label: "cat", confidence: 0.9 }]
|
|
}
|
|
});
|
|
expect(getByTestId("cat-confidence-set")).toBeInTheDocument();
|
|
});
|
|
|
|
test("set_data removing confidences hides all confidence bars", async () => {
|
|
const { set_data, queryByTestId } = await render(Label, default_props);
|
|
await set_data({ value: { label: "Cat" } });
|
|
expect(queryByTestId("cat-confidence-set")).not.toBeInTheDocument();
|
|
expect(queryByTestId("dog-confidence-set")).not.toBeInTheDocument();
|
|
expect(queryByTestId("lion-confidence-set")).not.toBeInTheDocument();
|
|
});
|
|
|
|
test("get_data returns the current value label", async () => {
|
|
const { set_data, get_data } = await render(Label, default_props);
|
|
await set_data({ value: { label: "Dog" } });
|
|
const data = await get_data();
|
|
expect(data.value.label).toBe("Dog");
|
|
});
|
|
|
|
test("round-trip: set_data value is returned unchanged by get_data", async () => {
|
|
const { set_data, get_data } = await render(Label, default_props);
|
|
const new_value = {
|
|
label: "bird",
|
|
confidences: [
|
|
{ label: "bird", confidence: 0.7 },
|
|
{ label: "fish", confidence: 0.3 }
|
|
]
|
|
};
|
|
await set_data({ value: new_value });
|
|
const data = await get_data();
|
|
expect(data.value).toEqual(new_value);
|
|
});
|
|
|
|
test("set_data with null label transitions to empty state", async () => {
|
|
const { set_data, queryByTestId } = await render(Label, default_props);
|
|
await set_data({ value: { label: null } });
|
|
expect(queryByTestId("label-output-value")).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("Accessibility", () => {
|
|
afterEach(() => cleanup());
|
|
|
|
test("confidence meter aria-valuenow reflects the confidence as a percentage integer", async () => {
|
|
const { getByRole } = await render(Label, {
|
|
...default_props,
|
|
value: {
|
|
label: "cat",
|
|
confidences: [{ label: "cat", confidence: 0.9 }]
|
|
}
|
|
});
|
|
expect(getByRole("meter", { name: "cat" })).toHaveAttribute(
|
|
"aria-valuenow",
|
|
"90"
|
|
);
|
|
});
|
|
|
|
test("confidence meter has aria-valuemin=0 and aria-valuemax=100", async () => {
|
|
const { getByRole } = await render(Label, {
|
|
...default_props,
|
|
value: {
|
|
label: "cat",
|
|
confidences: [{ label: "cat", confidence: 0.9 }]
|
|
}
|
|
});
|
|
const meter = getByRole("meter", { name: "cat" });
|
|
expect(meter).toHaveAttribute("aria-valuemin", "0");
|
|
expect(meter).toHaveAttribute("aria-valuemax", "100");
|
|
});
|
|
|
|
test("meter aria-labelledby references an existing element in the DOM", async () => {
|
|
const { getByRole } = await render(Label, {
|
|
...default_props,
|
|
value: {
|
|
label: "cat",
|
|
confidences: [{ label: "cat", confidence: 0.9 }]
|
|
}
|
|
});
|
|
const meter = getByRole("meter", { name: "cat" });
|
|
const labelledby_id = meter.getAttribute("aria-labelledby");
|
|
expect(labelledby_id).not.toBeNull();
|
|
expect(document.getElementById(labelledby_id!)).toBeInTheDocument();
|
|
});
|
|
|
|
test("confidence bars are accessible as button elements", async () => {
|
|
const { getAllByRole } = await render(Label, default_props);
|
|
expect(getAllByRole("button")).toHaveLength(3);
|
|
});
|
|
|
|
test("spaces in confidence label are replaced with hyphens in aria IDs (avoids invalid id-reference lists)", async () => {
|
|
const { getByRole } = await render(Label, {
|
|
...default_props,
|
|
value: {
|
|
label: "golden retriever",
|
|
confidences: [{ label: "golden retriever", confidence: 0.85 }]
|
|
}
|
|
});
|
|
const meter = getByRole("meter", { name: "golden retriever" });
|
|
// aria-labelledby must not contain spaces (each token is treated as a separate ID)
|
|
const labelledby_id = meter.getAttribute("aria-labelledby");
|
|
expect(labelledby_id).not.toContain(" ");
|
|
expect(document.getElementById(labelledby_id!)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe("Edge cases", () => {
|
|
afterEach(() => cleanup());
|
|
|
|
test("no spurious change event fires on initial mount", async () => {
|
|
const { listen } = await render(Label, default_props);
|
|
const change = listen("change", { retrospective: true });
|
|
expect(change).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("confidence of 0.5 displays as 50%", async () => {
|
|
const { getByText } = await render(Label, {
|
|
...default_props,
|
|
value: {
|
|
label: "cat",
|
|
confidences: [{ label: "cat", confidence: 0.5 }]
|
|
}
|
|
});
|
|
expect(getByText("50%")).toBeInTheDocument();
|
|
});
|
|
|
|
test("confidence of 0.855 rounds to 86% (not 85%)", async () => {
|
|
const { getByText, queryByText } = await render(Label, {
|
|
...default_props,
|
|
value: {
|
|
label: "cat",
|
|
confidences: [{ label: "cat", confidence: 0.855 }]
|
|
}
|
|
});
|
|
expect(getByText("86%")).toBeInTheDocument();
|
|
expect(queryByText("85%")).not.toBeInTheDocument();
|
|
});
|
|
|
|
test("very long label text renders the heading visibly", async () => {
|
|
const { getByTestId } = await render(Label, {
|
|
...default_props,
|
|
value: {
|
|
label:
|
|
"A very long classification label that describes many possible output categories in detail"
|
|
}
|
|
});
|
|
expect(getByTestId("label-output-value")).toBeVisible();
|
|
});
|
|
|
|
test("change event fires after set_data transitions from empty to labelled state", async () => {
|
|
const { listen, set_data } = await render(Label, {
|
|
...default_props,
|
|
value: { label: null }
|
|
});
|
|
const change = listen("change");
|
|
await set_data({ value: { label: "Cat" } });
|
|
expect(change).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|