Write comprehensive, behaviour-driven unit tests for Gradio frontend Svelte components using Vitest browser mode, Playwright, and the @self/tootils test utilities.
You are an expert at writing unit tests for Gradio's Svelte frontend components. Follow these instructions precisely.
Test everything. Unit tests are cheap. Having too many is a problem we want to have. When in doubt, write the test. Multiple tests per feature/argument is fine and encouraged.
Test behaviour, not implementation. Never assert on implementation details like CSS class names, internal state, or DOM structure for its own sake. Instead, test observable behaviour.
step attribute set to 55hiddentoBeVisible()Test Gradio-specific functionality. Every component has , , and dispatches events. These must be tested, including their interactions with props.
get_dataset_dataset_data -> verify the DOM reflects the changeget_data -> verify it returns the current stateset_data -> get_data round-tripsget_data reflects itchange, input, submit, blur, focus, clear, upload, select, custom_button_click, etc.Real browser environment. Tests run in Vitest browser mode with a Playwright provider. This is a real browser, not jsdom. Do not mock or stub unless absolutely unavoidable (e.g., navigator.clipboard, MediaStream). If you must mock, explain why in a comment.
Test sub-components in isolation when they have meaningful standalone logic (e.g., a utility function, a shared inner component). These tests are in addition to full Index.svelte integration tests.
Never refactor production code for testability without explicit user approval. If a refactor would help, recommend it and wait for a go-ahead.
Visual-only props get test.todo placeholders. If a prop or argument results in a purely visual change (colours, spacing, fonts, border styles, shadows, etc.) that cannot be meaningfully asserted with behavioural queries, do NOT skip it silently. Instead:
test.todo("description") explaining that it needs a visual regression testtest.todo(
"VISUAL: container_color='red' applies a red background to the component wrapper — needs Playwright visual regression screenshot comparison"
);
No useless comments. Comments should be used exceptionally, only when clarification of the code is essential. Do NOT create comments to describe types of tests (describe blocks do that). Do NOT add comments explaining the flow of the code (the code does that). Only add comments when something is confusing or complex, adds useful context (i.e. giving more detail on the failure case it is guarding against), or goes against our principles (this requires a comment + rationale).
All test utilities come from @self/tootils/render. Never import from @testing-library/svelte directly.
render(Component, props?, options?)Mounts a Gradio component with the full shared prop infrastructure (loading_status, dispatcher, i18n, etc.).
Returns:
container — the root DOM elementlisten(event_name, opts?) — returns a vi.fn() mock that records dispatched Gradio events. By default only captures events fired after the call. Use { retrospective: true } when testing mount-time events — this replays any events that were buffered during render before listen was called. Without this flag, mount-time events are invisible.set_data(data) — programmatically set component data (simulates backend push). Automatically ticks.get_data() — read current component data.@testing-library/dom query functions (getByRole, getByText, getByDisplayValue, queryByRole, etc.)debug() — prints pretty DOM to console.unmount() — teardown.Props are split automatically: keys in allowed_shared_props go to shared_props, everything else goes to props.
fireEventRe-exported from @testing-library/dom but wrapped to await tick() twice after each event (to let Svelte reactivity settle). Always await fireEvent calls.
cleanup()Call in afterEach to unmount all rendered components.
run_shared_prop_tests(config) (MANDATORY)Runs a standard suite of shared prop tests (elem_id, elem_classes, visible, label, show_label, validation_error). Every component test file MUST call this. Never manually re-implement these tests.
run_shared_prop_tests({
component: MyComponent,
name: "MyComponent",
base_props: { /* minimum props to render */ },
has_label: true, // default true; false for label-less components
has_validation_error: true // default true
});
When a shared test doesn't apply to a component (e.g., the component has no label), use the config flags to disable that specific test — do NOT skip run_shared_prop_tests entirely and rewrite everything by hand:
// Accordion has no label — disable label tests, keep everything else
run_shared_prop_tests({
component: Accordion,
name: "Accordion",
base_props: { label: "Section", open: true },
has_label: false,
has_validation_error: false
});
If a shared test fails for a component-specific reason that the flags don't cover, the correct response is to:
run_shared_prop_tests with appropriate flags to cover what it canupload_file(fixture, selector?) — sets files on a file input using real fixturesdrop_file(fixture, selector) — simulates drag-and-drop with real filesdownload_file(selector) — clicks an element and captures the downloadmock_client() — returns a mock client for components that use file uploads (the upload mock echoes input unchanged)Pre-built FileData objects pointing to real test files:
TEST_TXT, TEST_JPG, TEST_PNG, TEST_MP4, TEST_WAV, TEST_PDFFor keyboard/typing interactions, import @testing-library/user-event:
import event from "@testing-library/user-event";
el.focus();
await event.keyboard("some text");
await event.type(el, "123");
await event.clear(el);
Always use pnpm test:run. Never use pnpm test — it starts in watch mode and never exits.
All commands are run from the repo root.
# Run all unit tests
pnpm test:run
# Run a specific test file (match by filename)
pnpm test:run Textbox.test.ts
# Run all tests within a folder (match by path segment)
pnpm test:run dataframe
# Filter by test name with -t
pnpm test:run -t elem_id
# Combine file/folder filter with test name filter
pnpm test:run accordion -t elem_id
After writing or modifying tests, always run them to verify they pass.
import { test, describe, afterEach, expect, vi } from "vitest";
import { cleanup, render, fireEvent, waitFor } from "@self/tootils/render";
import { run_shared_prop_tests } from "@self/tootils/shared-prop-tests";
import event from "@testing-library/user-event";
import Component from "./Index.svelte";
const default_props = {
// Minimum props for a working render, always including:
label: "Component Name",
show_label: true,
interactive: true,
// ...component-specific props
};
// 1. Shared prop tests
run_shared_prop_tests({
component: Component,
name: "ComponentName",
base_props: { /* ... */ }
});
// 2. Describe blocks grouped by prop, feature, or concern
describe("ComponentName", () => {
afterEach(() => cleanup());
// General rendering and basic behaviour
});
describe("Props: propName", () => {
afterEach(() => cleanup());
// Tests for each meaningful prop value
});
describe("Events", () => {
afterEach(() => cleanup());
// change, input, submit, blur, focus, clear, etc.
});
describe("get_data / set_data", () => {
afterEach(() => cleanup());
// Round-trip, DOM reflection, interaction flow
});
describe("Edge cases", () => {
afterEach(() => cleanup());
// Null/undefined handling, deduplication, mount-time behaviour
});
Props: <name>, Events, Events: <name>, get_data / set_data, Edge cases, or component area."lines > 1 renders a textarea with correct rows", "change: emitted when value changes from outside".When asked to write tests for a specific feature, prop, or bug fix:
When asked to write or rewrite tests for an entire component:
You MUST follow this process:
Research phase — Read thoroughly:
Index.svelte (the main entry point)shared/ directorygradio/components/ to understand all props, events, and data typesdemo/ if they existAnalysis phase — Identify every testable surface:
get_data / set_data behaviourtest.todo with visual regression notesPlan phase — Present a structured testing plan:
test.todo placeholders for visual regression testingWait for approval — Present the plan and ask for feedback before writing code.
Implementation phase — Write the tests following the plan.
Always use the query utilities returned by render(). Never use container.querySelector unless every option below has been exhausted. Follow this priority order:
Semantic role queries (best — reflects how users and assistive tech see the component):
getByRole("textbox")
getByRole("button", { name: "Submit" })
getByRole("slider")
Label and text queries (good — reflects visible content):
getByLabelText("Upload file")
getByText("Submit")
getByDisplayValue("hello")
getByPlaceholderText("Enter text...")
Test ID queries (required fallback — when no semantic/text query works):
getByTestId("source-select")
getByTestId("password")
If the element lacks a data-testid, add one to the component source. This is always the right move. container.querySelector is never acceptable — adding a data-testid is cheap, explicit, and keeps test intent clear.
Use queryBy* variants (which return null instead of throwing) when asserting something is not in the DOM:
expect(queryByRole("button")).not.toBeInTheDocument();
expect(queryByLabelText("Upload file")).not.toBeInTheDocument();
// Visibility
expect(el).toBeVisible();
expect(el).not.toBeVisible();
// Presence
expect(queryByRole("button")).not.toBeInTheDocument(); // not in DOM
expect(getByRole("button")).toBeInTheDocument(); // in DOM
// Values
expect(el).toHaveValue("hello");
expect(el).toHaveAttribute("type", "password");
// State
expect(el).toBeDisabled();
expect(el).toBeEnabled();
expect(el).toHaveFocus();
// Events
const change = listen("change");
await set_data({ value: "new" });
expect(change).toHaveBeenCalledTimes(1);
expect(change).toHaveBeenCalledWith("new");
// Mount-time events — use { retrospective: true }
// listen() only captures events fired AFTER it is called. Since render()
// is awaited before listen() runs, any events fired during mount are missed.
// Pass { retrospective: true } to also replay events from the buffer.
//
// Use this whenever you need to assert about mount-time behaviour:
// - "no spurious change event on mount"
// - "component fires 'load' on mount"
// - "initial value triggers change on mount" (or doesn't)
const change = listen("change", { retrospective: true });
expect(change).not.toHaveBeenCalled(); // no spurious mount event
// If you expect an event WAS fired on mount:
const load = listen("load", { retrospective: true });
expect(load).toHaveBeenCalledTimes(1);
// Event deduplication
await set_data({ value: "x" });
await set_data({ value: "x" });
expect(change).toHaveBeenCalledTimes(1);
// Async operations (uploads, etc.)
await waitFor(() => {
expect(upload).toHaveBeenCalledTimes(1);
});
container.querySelector. It is unconditionally banned. Use getByRole, getByText, getByLabelText, getByDisplayValue, getByPlaceholderText, or getByTestId. If none of those work, add a data-testid attribute to the component source — this is always the correct solution.test.todo placeholders recommending Playwright visual regression tests. Do test behavioural effects of styling (visibility, disabled state)..sr-only for screen-reader-only labels).run_shared_prop_tests handles elem_id, elem_classes, visible, label, show_label, validation_error. Always call it. If a specific test doesn't apply, use the config flags (has_label: false, has_validation_error: false) — never skip the utility and hand-roll the tests instead.toBeTruthy() or toBeFalsy(). These are too vague and hide intent. Use the most specific matcher for the value being checked:
toBeInTheDocument() / .not.toBeInTheDocument()toBeVisible() / .not.toBeVisible()toHaveValue("x")toBeChecked()toBeDisabled() / toBeEnabled()toBe(true) / toBe(false)toHaveLength(n) or expect(arr.length).toBeGreaterThan(0)toBeNull() / toBeDefined() / toBeUndefined()setTimeout or artificial delays. Use await tick(), await fireEvent.x(), or await waitFor().@testing-library/svelte — always use @self/tootils/render.Study these files for patterns and quality bar:
js/textbox/Textbox.test.ts — comprehensive prop, event, and edge case testingjs/image/Image.test.ts — file upload/drop, sub-component isolation (get_coordinates_of_clicked_image), interactive vs static modes, custom buttons