// Copyright (c) Meta Platforms, Inc. and affiliates.

/**
 * @file Slider.test.tsx
 * @input Uses vitest, @testing-library/react, userEvent, Slider component
 * @output Unit tests for Slider component behavior
 * @position Testing; validates Slider.tsx implementation
 *
 * SYNC: When Slider.tsx changes, update tests to match new behavior
 */

import {useState} from 'react';
import {describe, it, expect, vi, beforeEach} from 'vitest';
import {render, screen, act, fireEvent, waitFor} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {Slider} from './Slider';

// Mock showPopover/hidePopover (not implemented in jsdom) so the tooltip layer
// reflects its open state via a `popover-open` attribute the tests can assert.
beforeEach(() => {
  HTMLElement.prototype.showPopover = vi.fn(function (this: HTMLElement) {
    this.setAttribute('popover-open', '');
    const event = new Event('toggle', {bubbles: false});
    Object.defineProperty(event, 'newState', {value: 'open'});
    this.dispatchEvent(event);
  });
  HTMLElement.prototype.hidePopover = vi.fn(function (this: HTMLElement) {
    this.removeAttribute('popover-open');
    const event = new Event('toggle', {bubbles: false});
    Object.defineProperty(event, 'newState', {value: 'closed'});
    this.dispatchEvent(event);
  });
  const originalMatches = HTMLElement.prototype.matches;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  (HTMLElement.prototype as any).matches = function (
    selector: string,
  ): boolean {
    if (selector === ':popover-open') {
      return this.hasAttribute('popover-open');
    }
    // jsdom does not derive :focus-visible from keyboard focus for a
    // div[role="slider"] thumb; treat the focused thumb as focus-visible so the
    // disabled-reason tooltip's keyboard-focus path can be exercised.
    if (selector === ':focus-visible') {
      return this === document.activeElement;
    }
    return originalMatches.call(this, selector);
  };
});

describe('Slider', () => {
  // --- Aria labels ---

  it('single thumb takes its accessible name from the label', () => {
    render(<Slider label="Volume" value={50} />);
    const slider = screen.getByRole('slider');
    expect(slider).toHaveAccessibleName('Volume');
  });

  it('range thumbs have individual names composing with the group label', () => {
    render(<Slider label="Price range" value={[20, 80] as [number, number]} />);
    const sliders = screen.getAllByRole('slider');
    expect(sliders[0]).toHaveAccessibleName('Minimum value');
    expect(sliders[1]).toHaveAccessibleName('Maximum value');
  });

  it('sets aria-valuetext with formatValue', () => {
    render(
      <Slider label="Temperature" value={72} formatValue={v => `${v}°F`} />,
    );
    const slider = screen.getByRole('slider');
    expect(slider).toHaveAttribute('aria-valuetext', '72°F');
  });

  it('uses custom min and max', () => {
    render(<Slider label="Temperature" value={72} min={60} max={90} />);
    const slider = screen.getByRole('slider');
    expect(slider).toHaveAttribute('aria-valuemin', '60');
    expect(slider).toHaveAttribute('aria-valuemax', '90');
    expect(slider).toHaveAttribute('aria-valuenow', '72');
  });

  it.each([
    {value: 150, expectedValue: 100, expectedPosition: '100%'},
    {value: -50, expectedValue: 0, expectedPosition: '0%'},
  ])(
    'clamps a controlled value of $value to $expectedValue',
    ({value, expectedValue, expectedPosition}) => {
      render(<Slider label="Volume" value={value} min={0} max={100} />);
      const slider = screen.getByRole('slider');
      expect(slider).toHaveAttribute('aria-valuenow', String(expectedValue));
      // Thumb positions via the logical `inset-inline-start` so it mirrors
      // under RTL (see RTL Phase 4). In LTR this resolves to the left edge.
      expect(slider).toHaveStyle({insetInlineStart: expectedPosition});
    },
  );

  it('range mode sets correct aria values on both thumbs', () => {
    render(
      <Slider
        label="Range"
        value={[25, 75] as [number, number]}
        min={0}
        max={100}
      />,
    );
    const sliders = screen.getAllByRole('slider');
    expect(sliders[0]).toHaveAttribute('aria-valuenow', '25');
    expect(sliders[1]).toHaveAttribute('aria-valuenow', '75');
    // Per the APG multi-thumb pattern, each thumb's bounds are constrained by
    // its sibling: the lower thumb can't exceed the upper thumb's value and
    // the upper thumb can't go below the lower thumb's value.
    expect(sliders[0]).toHaveAttribute('aria-valuemin', '0');
    expect(sliders[0]).toHaveAttribute('aria-valuemax', '75');
    expect(sliders[1]).toHaveAttribute('aria-valuemin', '25');
    expect(sliders[1]).toHaveAttribute('aria-valuemax', '100');
  });

  it('range thumb bounds update after moving a thumb', async () => {
    const user = userEvent.setup();
    function ControlledRange() {
      const [value, setValue] = useState<[number, number]>([25, 75]);
      return (
        <Slider
          label="Range"
          value={value}
          onChange={setValue}
          min={0}
          max={100}
        />
      );
    }
    render(<ControlledRange />);
    const sliders = screen.getAllByRole('slider');
    act(() => {
      sliders[0].focus();
    });
    await user.keyboard('{ArrowRight}');
    expect(sliders[0]).toHaveAttribute('aria-valuenow', '26');
    // The upper thumb's floor tracks the lower thumb's new value.
    expect(sliders[1]).toHaveAttribute('aria-valuemin', '26');
    expect(sliders[0]).toHaveAttribute('aria-valuemax', '75');
  });

  it('range thumb bounds include the minStepsBetweenThumbs gap', () => {
    render(
      <Slider
        label="Range"
        value={[20, 80] as [number, number]}
        min={0}
        max={100}
        step={5}
        minStepsBetweenThumbs={2}
      />,
    );
    const sliders = screen.getAllByRole('slider');
    // minGap = 2 steps * 5 = 10
    expect(sliders[0]).toHaveAttribute('aria-valuemax', '70');
    expect(sliders[1]).toHaveAttribute('aria-valuemin', '30');
  });

  // --- Label association ---

  // A <label htmlFor> must point at an existing form-associated element;
  // div[role="slider"] is not labelable, so the Slider label must render as a
  // group label instead (WCAG 1.3.1).
  const LABELABLE_TAGS = new Set([
    'BUTTON',
    'INPUT',
    'METER',
    'OUTPUT',
    'PROGRESS',
    'SELECT',
    'TEXTAREA',
  ]);

  function expectNoOrphanedLabels(container: HTMLElement) {
    for (const labelEl of container.querySelectorAll('label[for]')) {
      const target = document.getElementById(labelEl.getAttribute('for')!);
      expect(target).not.toBeNull();
      expect(LABELABLE_TAGS.has(target!.tagName)).toBe(true);
    }
  }

  it('single mode renders the label as a group label naming the thumb', () => {
    const {container} = render(<Slider label="Volume" value={50} />);
    expectNoOrphanedLabels(container);
    const slider = screen.getByRole('slider');
    const labelledBy = slider.getAttribute('aria-labelledby');
    expect(labelledBy).toBeTruthy();
    expect(document.getElementById(labelledBy!)).toHaveTextContent('Volume');
    expect(slider).toHaveAccessibleName('Volume');
  });

  it('range mode labels the slider group via aria-labelledby', () => {
    const {container} = render(
      <Slider label="Price range" value={[20, 80] as [number, number]} />,
    );
    expectNoOrphanedLabels(container);
    const group = screen.getByRole('group', {name: 'Price range'});
    const labelledBy = group.getAttribute('aria-labelledby');
    expect(labelledBy).toBeTruthy();
    expect(document.getElementById(labelledBy!)).toHaveTextContent(
      'Price range',
    );
  });

  it('sets aria-orientation for vertical', () => {
    render(<Slider label="Volume" value={50} orientation="vertical" />);
    const slider = screen.getByRole('slider');
    expect(slider).toHaveAttribute('aria-orientation', 'vertical');
  });

  it('sets aria-invalid when status type is error', () => {
    render(
      <Slider
        label="Volume"
        value={50}
        status={{type: 'error', message: 'Value too high'}}
      />,
    );
    const slider = screen.getByRole('slider');
    expect(slider).toHaveAttribute('aria-invalid', 'true');
  });

  it('associates description via aria-describedby', () => {
    render(
      <Slider
        label="Volume"
        value={50}
        description="Adjust the volume level"
      />,
    );
    const slider = screen.getByRole('slider');
    const describedby = slider.getAttribute('aria-describedby');
    expect(describedby).toBeTruthy();
    const descEl = document.getElementById(describedby!.split(' ')[0]);
    expect(descEl).toHaveTextContent('Adjust the volume level');
  });

  it('associates status message via aria-describedby', () => {
    render(
      <Slider
        label="Volume"
        value={50}
        description="Adjust the volume level"
        status={{type: 'error', message: 'Too loud'}}
      />,
    );
    const slider = screen.getByRole('slider');
    const describedby = slider.getAttribute('aria-describedby');
    expect(describedby).toBeTruthy();
    // Should have at least two IDs (description + status message)
    const ids = describedby!.split(' ');
    expect(ids.length).toBeGreaterThanOrEqual(2);
  });

  it('decorative track elements have aria-hidden', () => {
    const {container} = render(<Slider label="Volume" value={50} />);
    const ariaHidden = container.querySelectorAll('[aria-hidden="true"]');
    expect(ariaHidden.length).toBeGreaterThanOrEqual(2);
  });

  // --- Required state ---

  it('conveys required state through the accessible description', () => {
    render(<Slider label="Volume" value={50} isRequired />);
    const slider = screen.getByRole('slider');
    expect(slider).toHaveAccessibleDescription(/Required/);
    // aria-required is not a supported property of role="slider" in
    // WAI-ARIA 1.2, so it must never appear on the thumb.
    expect(slider).not.toHaveAttribute('aria-required');
  });

  it('conveys required state on both thumbs of a range slider', () => {
    render(
      <Slider
        label="Price range"
        value={[20, 80] as [number, number]}
        isRequired
      />,
    );
    const sliders = screen.getAllByRole('slider');
    expect(sliders).toHaveLength(2);
    for (const thumb of sliders) {
      expect(thumb).toHaveAccessibleDescription(/Required/);
      expect(thumb).not.toHaveAttribute('aria-required');
    }
  });

  it('combines required with other describedby parts in the description', () => {
    render(
      <Slider
        label="Volume"
        value={50}
        description="Adjust the volume level"
        isRequired
      />,
    );
    const slider = screen.getByRole('slider');
    expect(slider).toHaveAccessibleDescription(/Adjust the volume level/);
    expect(slider).toHaveAccessibleDescription(/Required/);
  });

  it('does not mention required without isRequired', () => {
    render(
      <Slider
        label="Volume"
        value={50}
        description="Adjust the volume level"
      />,
    );
    const slider = screen.getByRole('slider');
    expect(slider).not.toHaveAccessibleDescription(/Required/);
    expect(slider).not.toHaveAttribute('aria-required');
  });

  // --- Disabled guards ---

  it('disables thumbs when isDisabled is true', () => {
    render(<Slider label="Volume" value={50} isDisabled />);
    const slider = screen.getByRole('slider');
    expect(slider).toHaveAttribute('aria-disabled', 'true');
    expect(slider).toHaveAttribute('tabIndex', '-1');
  });

  it('does not fire onChange on pointer down when disabled', () => {
    const handleChange = vi.fn();
    render(
      <Slider
        label="Volume"
        value={50}
        min={0}
        max={100}
        onChange={handleChange}
        isDisabled
      />,
    );
    const slider = screen.getByRole('slider');
    const trackContainer = slider.parentElement!;

    trackContainer.getBoundingClientRect = () => ({
      left: 0,
      top: 0,
      right: 200,
      bottom: 20,
      width: 200,
      height: 20,
      x: 0,
      y: 0,
      toJSON: () => {},
    });

    fireEvent.pointerDown(trackContainer, {
      clientX: 100,
      clientY: 10,
      pointerId: 1,
    });

    expect(handleChange).not.toHaveBeenCalled();
  });

  it('does not fire onChange on keyboard when disabled', () => {
    const handleChange = vi.fn();
    render(
      <Slider label="Volume" value={50} onChange={handleChange} isDisabled />,
    );
    const slider = screen.getByRole('slider');
    fireEvent.keyDown(slider, {key: 'ArrowRight'});
    expect(handleChange).not.toHaveBeenCalled();
  });

  // --- onChangeEnd on keyboard ---

  it('fires onChangeEnd on keyboard ArrowRight', async () => {
    const user = userEvent.setup();
    const handleChange = vi.fn();
    const handleChangeEnd = vi.fn();
    render(
      <Slider
        label="Volume"
        value={50}
        step={5}
        onChange={handleChange}
        onChangeEnd={handleChangeEnd}
      />,
    );
    const slider = screen.getByRole('slider');
    act(() => {
      slider.focus();
    });
    await user.keyboard('{ArrowRight}');
    expect(handleChange).toHaveBeenCalledWith(55);
    expect(handleChangeEnd).toHaveBeenCalledWith(55);
  });

  // --- Fractional step precision ---

  it('emits exact decimal values for fractional steps on keyboard', async () => {
    const user = userEvent.setup();
    const handleChange = vi.fn();
    const handleChangeEnd = vi.fn();
    render(
      <Slider
        label="Opacity"
        value={0.2}
        min={0}
        max={1}
        step={0.1}
        onChange={handleChange}
        onChangeEnd={handleChangeEnd}
      />,
    );
    const slider = screen.getByRole('slider');
    act(() => {
      slider.focus();
    });
    await user.keyboard('{ArrowRight}');
    // 0.2 + 0.1 must not surface binary float error (0.30000000000000004)
    expect(handleChange).toHaveBeenCalledWith(0.3);
    expect(handleChangeEnd).toHaveBeenCalledWith(0.3);
  });

  it('emits exact decimal values for fractional steps in range mode', async () => {
    const user = userEvent.setup();
    const handleChange = vi.fn();
    const handleChangeEnd = vi.fn();
    render(
      <Slider
        label="Range"
        value={[0.2, 0.6] as [number, number]}
        min={0}
        max={1}
        step={0.1}
        onChange={handleChange}
        onChangeEnd={handleChangeEnd}
      />,
    );
    const sliders = screen.getAllByRole('slider');
    act(() => {
      sliders[1].focus();
    });
    await user.keyboard('{ArrowRight}');
    // 0.6 + 0.1 snaps to 7 * 0.1, which is 0.7000000000000001 without rounding
    expect(handleChange).toHaveBeenCalledWith([0.2, 0.7]);
    expect(handleChangeEnd).toHaveBeenCalledWith([0.2, 0.7]);
  });

  it('fires onChangeEnd on keyboard Home/End with correct value', async () => {
    const user = userEvent.setup();
    const handleChangeEnd = vi.fn();
    render(
      <Slider
        label="Volume"
        value={50}
        min={0}
        max={100}
        onChange={vi.fn()}
        onChangeEnd={handleChangeEnd}
      />,
    );
    const slider = screen.getByRole('slider');
    act(() => {
      slider.focus();
    });
    await user.keyboard('{Home}');
    expect(handleChangeEnd).toHaveBeenCalledWith(0);
  });

  it('fires onChangeEnd with correct value for range mode on keyboard', async () => {
    const user = userEvent.setup();
    const handleChangeEnd = vi.fn();
    render(
      <Slider
        label="Range"
        value={[20, 80] as [number, number]}
        min={0}
        max={100}
        step={5}
        onChange={vi.fn()}
        onChangeEnd={handleChangeEnd}
      />,
    );
    const sliders = screen.getAllByRole('slider');
    act(() => {
      sliders[0].focus();
    });
    await user.keyboard('{ArrowRight}');
    expect(handleChangeEnd).toHaveBeenCalledWith([25, 80]);
  });

  // --- Pointer handling ---

  it('fires onChangeEnd on pointer up after pointer down', () => {
    const handleChange = vi.fn();
    const handleChangeEnd = vi.fn();
    render(
      <Slider
        label="Volume"
        value={50}
        min={0}
        max={100}
        onChange={handleChange}
        onChangeEnd={handleChangeEnd}
      />,
    );
    const slider = screen.getByRole('slider');
    const trackContainer = slider.parentElement!;

    trackContainer.getBoundingClientRect = () => ({
      left: 0,
      top: 0,
      right: 200,
      bottom: 20,
      width: 200,
      height: 20,
      x: 0,
      y: 0,
      toJSON: () => {},
    });

    fireEvent.pointerDown(trackContainer, {
      clientX: 100,
      clientY: 10,
      pointerId: 1,
    });
    fireEvent.pointerUp(trackContainer, {
      clientX: 100,
      clientY: 10,
      pointerId: 1,
    });

    expect(handleChangeEnd).toHaveBeenCalledTimes(1);
  });

  it('focuses closest thumb on track click', () => {
    render(
      <Slider label="Volume" value={50} min={0} max={100} onChange={vi.fn()} />,
    );
    const slider = screen.getByRole('slider');
    const trackContainer = slider.parentElement!;

    trackContainer.getBoundingClientRect = () => ({
      left: 0,
      top: 0,
      right: 200,
      bottom: 20,
      width: 200,
      height: 20,
      x: 0,
      y: 0,
      toJSON: () => {},
    });

    fireEvent.pointerDown(trackContainer, {
      clientX: 100,
      clientY: 10,
      pointerId: 1,
    });

    expect(document.activeElement).toBe(slider);
  });

  // --- RTL pointer value mapping (RTL Phase 4) ---
  // Contract: the inline-start edge (value = min) is the RIGHT edge under RTL,
  // so the same physical click maps to the mirrored value relative to LTR.
  // `valueDisplay="none"` keeps the thumb a direct child of the track element
  // (no Tooltip wrapper) so the mocked rect lands on trackRef.

  it('maps a track click to the LTR value in the default direction', () => {
    const handleChange = vi.fn();
    render(
      <Slider
        label="Volume"
        value={0}
        min={0}
        max={100}
        valueDisplay="none"
        onChange={handleChange}
      />,
    );
    const track = screen.getByRole('slider').parentElement!;
    track.getBoundingClientRect = () =>
      ({
        left: 0,
        top: 0,
        right: 200,
        bottom: 20,
        width: 200,
        height: 20,
        x: 0,
        y: 0,
        toJSON: () => {},
      });

    // Click at 25% of the track from the left (x=50 of 200) → value 25 in LTR.
    fireEvent.pointerDown(track, {clientX: 50, clientY: 10, pointerId: 1});
    expect(handleChange).toHaveBeenLastCalledWith(25);
  });

  it('mirrors a track click to the RTL value when the track is rtl', () => {
    const handleChange = vi.fn();
    render(
      <Slider
        label="Volume"
        value={0}
        min={0}
        max={100}
        valueDisplay="none"
        onChange={handleChange}
      />,
    );
    const track = screen.getByRole('slider').parentElement!;
    track.getBoundingClientRect = () =>
      ({
        left: 0,
        top: 0,
        right: 200,
        bottom: 20,
        width: 200,
        height: 20,
        x: 0,
        y: 0,
        toJSON: () => {},
      });

    // Force the track's computed direction to rtl (isRtlElement reads this).
    const realGetComputedStyle = window.getComputedStyle;
    const gcsSpy = vi
      .spyOn(window, 'getComputedStyle')
      .mockImplementation((el: Element, pseudo?: string | null) => {
        if (el === track) {
          return {direction: 'rtl'} as CSSStyleDeclaration;
        }
        return realGetComputedStyle(el, pseudo ?? undefined);
      });

    // Same physical click at 25% from the left (x=50). Under RTL the inline
    // start is the right edge, so fraction = (right - x)/width = 150/200 = 0.75
    // → value 75 (the mirror of the LTR value 25).
    fireEvent.pointerDown(track, {clientX: 50, clientY: 10, pointerId: 1});
    expect(handleChange).toHaveBeenLastCalledWith(75);

    gcsSpy.mockRestore();
  });

  // --- Mark label click snapping ---

  it('clicking a mark label snaps to that mark value, not pointer position', () => {
    const handleChange = vi.fn();
    render(
      <Slider
        label="Volume"
        value={50}
        min={0}
        max={100}
        onChange={handleChange}
        marks={[{value: 100, label: '100'}]}
      />,
    );
    const markLabel = screen.getByTestId('slider-mark-label');

    // Simulate a click on the left edge of the "100" label — pointer X would
    // map to ~99 if calculated from position, but should snap to 100.
    fireEvent.pointerDown(markLabel, {clientX: 1, clientY: 10, pointerId: 1});

    expect(handleChange).toHaveBeenCalledWith(100);
  });

  // --- Boundary clamping ---

  it('clamps value at max boundary', async () => {
    const user = userEvent.setup();
    const handleChange = vi.fn();
    render(
      <Slider
        label="Volume"
        value={99}
        min={0}
        max={100}
        step={5}
        onChange={handleChange}
      />,
    );
    const slider = screen.getByRole('slider');
    act(() => {
      slider.focus();
    });
    await user.keyboard('{ArrowRight}');
    expect(handleChange).toHaveBeenCalledWith(100);
  });

  it('clamps value at min boundary', async () => {
    const user = userEvent.setup();
    const handleChange = vi.fn();
    render(
      <Slider
        label="Volume"
        value={1}
        min={0}
        max={100}
        step={5}
        onChange={handleChange}
      />,
    );
    const slider = screen.getByRole('slider');
    act(() => {
      slider.focus();
    });
    await user.keyboard('{ArrowLeft}');
    expect(handleChange).toHaveBeenCalledWith(0);
  });

  describe('disabledMessage', () => {
    const h = {hidden: true} as const;

    function getTrack(): HTMLElement {
      return screen.getByRole('slider').parentElement!;
    }

    it('shows the reason tooltip on hover when disabled with a reason', async () => {
      render(
        <Slider
          label="Volume"
          value={50}
          valueDisplay="none"
          isDisabled
          disabledMessage="Volume is locked while sharing your screen"
        />,
      );
      const tooltip = screen.getByRole('tooltip', h);
      expect(tooltip).toHaveTextContent(
        'Volume is locked while sharing your screen',
      );
      fireEvent.mouseEnter(getTrack());
      await waitFor(() => expect(tooltip).toHaveAttribute('popover-open'));
      fireEvent.mouseLeave(getTrack());
      await waitFor(() => expect(tooltip).not.toHaveAttribute('popover-open'));
    });

    it('shows the reason tooltip on keyboard focus', async () => {
      const user = userEvent.setup();
      render(
        <Slider
          label="Volume"
          value={50}
          valueDisplay="none"
          isDisabled
          disabledMessage="Volume is locked while sharing your screen"
        />,
      );
      const tooltip = screen.getByRole('tooltip', h);
      await user.tab();
      expect(screen.getByRole('slider')).toHaveFocus();
      await waitFor(() => expect(tooltip).toHaveAttribute('popover-open'));
    });

    it('does not render a tooltip when not disabled', () => {
      render(
        <Slider
          label="Volume"
          value={50}
          valueDisplay="none"
          disabledMessage="Volume is locked while sharing your screen"
        />,
      );
      expect(screen.queryByRole('tooltip', h)).not.toBeInTheDocument();
    });

    it('does not render a tooltip when disabled without a reason', () => {
      render(
        <Slider label="Volume" value={50} valueDisplay="none" isDisabled />,
      );
      expect(screen.queryByRole('tooltip', h)).not.toBeInTheDocument();
    });

    it('keeps the thumb focusable via aria-disabled when a reason is provided', () => {
      render(
        <Slider
          label="Volume"
          value={50}
          valueDisplay="none"
          isDisabled
          disabledMessage="Volume is locked while sharing your screen"
        />,
      );
      const thumb = screen.getByRole('slider');
      expect(thumb).toHaveAttribute('aria-disabled', 'true');
      expect(thumb).toHaveAttribute('tabindex', '0');
    });

    it('links the reason tooltip via aria-describedby', () => {
      render(
        <Slider
          label="Volume"
          value={50}
          valueDisplay="none"
          isDisabled
          disabledMessage="Volume is locked while sharing your screen"
        />,
      );
      const thumb = screen.getByRole('slider');
      const tooltip = screen.getByRole('tooltip', h);
      expect(thumb.getAttribute('aria-describedby')).toContain(tooltip.id);
    });

    it('blocks value changes while focusable-disabled', async () => {
      const user = userEvent.setup();
      const onChange = vi.fn();
      render(
        <Slider
          label="Volume"
          value={50}
          valueDisplay="none"
          onChange={onChange}
          isDisabled
          disabledMessage="Volume is locked while sharing your screen"
        />,
      );
      const thumb = screen.getByRole('slider');
      thumb.focus();
      await user.keyboard('{ArrowRight}');
      expect(onChange).not.toHaveBeenCalled();
    });

    it('remains non-focusable when disabled without a reason', () => {
      render(
        <Slider label="Volume" value={50} valueDisplay="none" isDisabled />,
      );
      expect(screen.getByRole('slider')).toHaveAttribute('tabindex', '-1');
    });
  });
  describe('form participation', () => {
    it('submits the value under htmlName', () => {
      const {container} = render(
        <form>
          <Slider label="Volume" htmlName="volume" value={50} />
        </form>,
      );
      const data = new FormData(container.querySelector('form')!);
      expect(data.get('volume')).toBe('50');
    });

    it('submits both range values under the same name', () => {
      const {container} = render(
        <form>
          <Slider
            label="Price"
            htmlName="price"
            value={[20, 80] as [number, number]}
          />
        </form>,
      );
      const data = new FormData(container.querySelector('form')!);
      expect(data.getAll('price')).toEqual(['20', '80']);
    });

    it('is excluded from form data when disabled', () => {
      const {container} = render(
        <form>
          <Slider label="Volume" htmlName="volume" value={50} isDisabled />
        </form>,
      );
      expect([
        ...new FormData(container.querySelector('form')!).keys(),
      ]).toEqual([]);
    });
  });
});
