/* Copyright 2026 Marimo. All rights reserved. */
import { act, fireEvent, render } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { z } from "zod";
import { SetupMocks } from "@/__mocks__/common";
import { initialModeAtom } from "@/core/mode";
import { store } from "@/core/state/jotai";
import type { IPluginProps } from "../../types";
import { SliderPlugin } from "../SliderPlugin";
vi.mock("@/components/ui/slider", () => ({
Slider: ({
disabled,
onValueChange,
onValueCommit,
value,
}: {
disabled?: boolean;
onValueChange?: (value: number[]) => void;
onValueCommit?: (value: number[]) => void;
value: number[];
}) => (
),
}));
SetupMocks.resizeObserver();
describe("SliderPlugin", () => {
beforeEach(() => {
vi.useFakeTimers();
store.set(initialModeAtom, "edit");
});
afterEach(() => {
vi.useRealTimers();
});
const createProps = (
debounce: boolean,
includeInput: boolean,
setValue: ReturnType,
): IPluginProps> => {
return {
host: document.createElement("div"),
value: 5,
setValue,
data: {
initialValue: 5,
start: 0,
stop: 10,
step: 1,
label: "Test Slider",
debounce,
orientation: "horizontal" as const,
showValue: false,
fullWidth: false,
includeInput,
steps: null,
},
functions: {},
};
};
it("slider triggers setValue immediately when debounce is false", () => {
const plugin = new SliderPlugin();
const setValue = vi.fn();
const props = createProps(false, false, setValue);
const { getByRole } = render(plugin.render(props));
act(() => {
vi.advanceTimersByTime(0);
});
const changeButton = getByRole("button", { name: "Slider change" });
act(() => {
fireEvent.click(changeButton);
});
expect(setValue).toHaveBeenCalledWith(6);
});
it("slider waits until commit before calling setValue when debounce is true", () => {
const plugin = new SliderPlugin();
const setValue = vi.fn();
const props = createProps(true, false, setValue);
const { getByRole } = render(plugin.render(props));
act(() => {
vi.advanceTimersByTime(0);
});
const changeButton = getByRole("button", { name: "Slider change" });
const commitButton = getByRole("button", { name: "Slider commit" });
act(() => {
fireEvent.click(changeButton);
});
expect(setValue).not.toHaveBeenCalled();
act(() => {
fireEvent.click(commitButton);
});
expect(setValue).toHaveBeenCalledWith(6);
});
it("editable input triggers setValue immediately even when slider debounce is true", () => {
const plugin = new SliderPlugin();
const setValue = vi.fn();
const props = createProps(true, true, setValue);
const { getByRole } = render(plugin.render(props));
act(() => {
vi.advanceTimersByTime(0);
});
// The react-aria NumberField renders an input textbox.
const numericInput = getByRole("textbox");
act(() => {
// Simulate typing a new value and pressing enter
// With React-Aria NumberField, onChange fires on blur or enter
fireEvent.change(numericInput, { target: { value: "9" } });
fireEvent.blur(numericInput);
});
// Because the user explicitly typed 9 in the editable input,
// setValue should be called immediately regardless of debounce=true.
expect(setValue).toHaveBeenCalledWith(9);
});
});