Repository: holoviz/panel

Issue:
Editable slider widgets should have hard bounds
Currently the editable slider widget have one regular set of bounds defined as `start` and `end` which define the limits of the slider. The text input does not have any bounds applied to it however which is problematic because you may want to define hard bounds.


Discussion:

Resolving patch:
diff --git a/panel/param.py b/panel/param.py
--- a/panel/param.py
+++ b/panel/param.py
@@ -456,6 +456,10 @@ def widget(self, p_name):
                         widget_class = LiteralInput
             if hasattr(widget_class, 'step') and getattr(p_obj, 'step', None):
                 kw['step'] = p_obj.step
+            if hasattr(widget_class, 'fixed_start') and getattr(p_obj, 'bounds', None):
+                kw['fixed_start'] = p_obj.bounds[0]
+            if hasattr(widget_class, 'fixed_end') and getattr(p_obj, 'bounds', None):
+                kw['fixed_end'] = p_obj.bounds[1]
 
         kwargs = {k: v for k, v in kw.items() if k in widget_class.param}
 
diff --git a/panel/widgets/slider.py b/panel/widgets/slider.py
--- a/panel/widgets/slider.py
+++ b/panel/widgets/slider.py
@@ -779,7 +779,7 @@ def __init__(self, **params):
         super().__init__(**params)
         self._label = StaticText(margin=0, align='end')
         self._slider = self._slider_widget(
-            value=self.value, margin=(0, 0, 5, 0), sizing_mode='stretch_width'
+            value=self.value, margin=(0, 0, 5, 0), sizing_mode='stretch_width',
         )
         self._slider.param.watch(self._sync_value, 'value')
         self._slider.param.watch(self._sync_value, 'value_throttled')
@@ -803,6 +803,7 @@ def __init__(self, **params):
         self._update_name()
         self._update_slider()
         self._update_value()
+        self._update_bounds()
 
     @param.depends('width', 'height', 'sizing_mode', watch=True)
     def _update_layout(self):
@@ -849,6 +850,24 @@ def _sync_value(self, event):
         with param.edit_constant(self):
             self.param.update(**{event.name: event.new})
 
+    @param.depends("start", "end", "fixed_start", "fixed_end", watch=True)
+    def _update_bounds(self):
+        self.param.value.softbounds = (self.start, self.end)
+        self.param.value_throttled.softbounds = (self.start, self.end)
+        self.param.value.bounds = (self.fixed_start, self.fixed_end)
+        self.param.value_throttled.bounds = (self.fixed_start, self.fixed_end)
+
+        # First changing _slider and then _value_edit,
+        # because else _value_edit will change the _slider
+        # with the jscallback.
+        if self.fixed_start is not None:
+            self._slider.start = max(self.fixed_start, self.start)
+        if self.fixed_end is not None:
+            self._slider.end = min(self.fixed_end, self.end)
+
+        self._value_edit.start = self.fixed_start
+        self._value_edit.end = self.fixed_end
+
 
 class EditableFloatSlider(_EditableContinuousSlider, FloatSlider):
     """
@@ -865,6 +884,12 @@ class EditableFloatSlider(_EditableContinuousSlider, FloatSlider):
     ... )
     """
 
+    fixed_start = param.Number(default=None, doc="""
+        A fixed lower bound for the slider and input.""")
+
+    fixed_end = param.Number(default=None, doc="""
+        A fixed upper bound for the slider and input.""")
+
     _slider_widget: ClassVar[Type[Widget]] = FloatSlider
     _input_widget: ClassVar[Type[Widget]] = FloatInput
 
@@ -884,6 +909,12 @@ class EditableIntSlider(_EditableContinuousSlider, IntSlider):
     ... )
     """
 
+    fixed_start = param.Integer(default=None, doc="""
+        A fixed lower bound for the slider and input.""")
+
+    fixed_end = param.Integer(default=None, doc="""
+       A fixed upper bound for the slider and input.""")
+
     _slider_widget: ClassVar[Type[Widget]] = IntSlider
     _input_widget: ClassVar[Type[Widget]] = IntInput
 
@@ -912,6 +943,12 @@ class EditableRangeSlider(CompositeWidget, _SliderBase):
 
     end = param.Number(default=1., doc="Upper bound of the range.")
 
+    fixed_start = param.Number(default=None, doc="""
+        A fixed lower bound for the slider and input.""")
+
+    fixed_end = param.Number(default=None, doc="""
+        A fixed upper bound for the slider and input.""")
+
     step = param.Number(default=0.1, doc="Slider and number input step.")
 
     editable = param.Tuple(default=(True, True), doc="""
@@ -946,19 +983,24 @@ def __init__(self, **params):
         edit = Row(self._label, self._start_edit, sep, self._end_edit,
                    sizing_mode='stretch_width', margin=0)
         self._composite.extend([edit, self._slider])
-        self._slider.jscallback(args={'start': self._start_edit, 'end': self._end_edit}, value="""
-        let [min, max] = cb_obj.value
-        start.value = min
-        end.value = max
-        """)
-        self._start_edit.jscallback(args={'slider': self._slider}, value="""
+        self._start_edit.jscallback(args={'slider': self._slider, 'end': self._end_edit}, value="""
+        // start value always smaller than the end value
+        if (cb_obj.value >= end.value) {
+          cb_obj.value = end.value
+          return
+        }
         if (cb_obj.value < slider.start) {
           slider.start = cb_obj.value
         } else if (cb_obj.value > slider.end) {
           slider.end = cb_obj.value
         }
         """)
-        self._end_edit.jscallback(args={'slider': self._slider}, value="""
+        self._end_edit.jscallback(args={'slider': self._slider ,'start': self._start_edit}, value="""
+        // end value always larger than the start value
+        if (cb_obj.value <= start.value) {
+          cb_obj.value = start.value
+          return
+        }
         if (cb_obj.value < slider.start) {
           slider.start = cb_obj.value
         } else if (cb_obj.value > slider.end) {
@@ -970,6 +1012,7 @@ def __init__(self, **params):
         self._update_name()
         self._update_slider()
         self._update_value()
+        self._update_bounds()
 
     @param.depends('editable', watch=True)
     def _update_editable(self):
@@ -1040,3 +1083,23 @@ def _sync_end_value(self, event):
             self.param.update(
                 **{event.name: (start, event.new)}
             )
+
+    @param.depends("start", "end", "fixed_start", "fixed_end", watch=True)
+    def _update_bounds(self):
+        self.param.value.softbounds = (self.start, self.end)
+        self.param.value_throttled.softbounds = (self.start, self.end)
+        self.param.value.bounds = (self.fixed_start, self.fixed_end)
+        self.param.value_throttled.bounds = (self.fixed_start, self.fixed_end)
+
+        # First changing _slider and then _value_edit,
+        # because else _value_edit will change the _slider
+        # with the jscallback.
+        if self.fixed_start is not None:
+            self._slider.start = max(self.fixed_start, self.start)
+        if self.fixed_end is not None:
+            self._slider.end = min(self.fixed_end, self.end)
+
+        self._start_edit.start = self.fixed_start
+        self._start_edit.end = self.fixed_end
+        self._end_edit.start = self.fixed_start
+        self._end_edit.end = self.fixed_end


Test patch:
diff --git a/panel/tests/test_param.py b/panel/tests/test_param.py
--- a/panel/tests/test_param.py
+++ b/panel/tests/test_param.py
@@ -1,6 +1,7 @@
 import os
 
 import param
+import pytest
 
 from bokeh.models import (
     AutocompleteInput as BkAutocompleteInput, Button, CheckboxGroup,
@@ -18,8 +19,8 @@
 )
 from panel.tests.util import mpl_available, mpl_figure
 from panel.widgets import (
-    AutocompleteInput, DatePicker, DatetimeInput, LiteralInput, NumberInput,
-    RangeSlider,
+    AutocompleteInput, DatePicker, DatetimeInput, EditableFloatSlider,
+    EditableRangeSlider, LiteralInput, NumberInput, RangeSlider,
 )
 
 
@@ -1361,3 +1362,33 @@ def foo():
 
     for log_record in caplog.records:
         assert "The function 'foo' does not have any dependencies and will never update" not in log_record.message
+
+
+def test_param_editablefloatslider_with_bounds():
+    class Test(param.Parameterized):
+        i = param.Number(default=1, softbounds=(1, 5), bounds=(0, 10))
+
+
+    t = Test()
+    w = EditableFloatSlider.from_param(t.param.i)
+
+    msg = "Parameter 'value' must be at least 0, not -1"
+    with pytest.raises(ValueError, match=msg):
+        w.value = -1
+
+    assert w.value == 1
+
+
+def test_param_editablerangeslider_with_bounds():
+    class Test(param.Parameterized):
+        i = param.Range(default=(1, 2), softbounds=(1, 5), bounds=(0, 10))
+
+
+    t = Test()
+    w = EditableRangeSlider.from_param(t.param.i)
+
+    msg = "Range parameter 'value''s lower bound must be in range \[0, 10\]"
+    with pytest.raises(ValueError, match=msg):
+        w.value = (-1, 2)
+
+    assert w.value == (1, 2)
diff --git a/panel/tests/ui/widgets/test_sliders.py b/panel/tests/ui/widgets/test_sliders.py
new file mode 100644
--- /dev/null
+++ b/panel/tests/ui/widgets/test_sliders.py
@@ -0,0 +1,337 @@
+import time
+
+import pytest
+
+try:
+    import playwright  # noqa
+except ImportError:
+    pytestmark = pytest.mark.skip('playwright not available')
+
+pytestmark = pytest.mark.ui
+
+from panel.io.server import serve
+from panel.tests.util import wait_until
+from panel.widgets import (
+    EditableFloatSlider, EditableIntSlider, EditableRangeSlider,
+)
+
+
+class _editable_text_input:
+    """
+    This function is added for convenience, as it is pretty
+    cumbersome to find and update the value of Editable text input.
+    """
+
+    def __init__(self, page, nth=0):
+        self.page = page
+        self.text_input = page.locator("[placeholder=\"\\30 \"]").nth(nth)
+
+    @property
+    def value(self):
+        # Needs reload of page to update attribute
+        # Should be reimplemented without the need for update
+        self.page.reload()
+        return self.text_input.get_attribute("value")
+
+    @value.setter
+    def value(self, value):
+        self.text_input.fill(str(value))
+        self.text_input.press("Enter")
+
+
+@pytest.mark.parametrize(
+    "widget,val1,val2,val3,func",
+    [
+       (EditableIntSlider, 25, 24, 20, int),
+       (EditableFloatSlider, 25.5, 24.5, 19.5, float),
+    ],
+    ids=["EditableIntSlider", "EditableFloatSlider"]
+)
+def test_editableslider_textinput_end(page, port, widget, val1, val2, val3, func):
+    widget = widget()
+
+    serve(widget, port=port, threaded=True, show=False)
+    time.sleep(0.2)
+
+    page.goto(f"http://localhost:{port}")
+
+    text_input = _editable_text_input(page)
+
+    text_input.value = val1
+    wait_until(page, lambda: widget.value == val1)
+    assert widget._slider.end == val1
+    assert func(text_input.value) == val1
+
+    text_input.value = val2
+    wait_until(page, lambda: widget.value == val2)
+    assert widget._slider.end == val1
+    assert func(text_input.value) == val2
+
+    # Setting fixed end
+    widget.fixed_end = val3
+    wait_until(page, lambda: widget.value == val3)
+    assert widget._slider.end == val3
+    assert func(text_input.value) == val3
+
+@pytest.mark.parametrize(
+    "widget,val1,val2,val3,func",
+    [
+       (EditableIntSlider, -25, -24, -20, int),
+       (EditableFloatSlider, -25.5, -24.5, -19.5, float),
+    ],
+    ids=["EditableIntSlider", "EditableFloatSlider"]
+)
+def test_editableslider_textinput_start(page, port, widget, val1, val2, val3, func):
+    widget = widget()
+
+    serve(widget, port=port, threaded=True, show=False)
+    time.sleep(0.2)
+
+    page.goto(f"http://localhost:{port}")
+
+    text_input = _editable_text_input(page)
+
+    text_input.value = val1
+    wait_until(page, lambda: widget.value == val1)
+    assert widget._slider.start == val1
+    assert func(text_input.value) == val1
+
+    text_input.value = val2
+    wait_until(page, lambda: widget.value == val2)
+    assert widget._slider.start == val1
+    assert func(text_input.value) == val2
+
+    # Setting fixed start
+    widget.fixed_start = val3
+    wait_until(page, lambda: widget.value == val3)
+    assert widget._slider.start == val3
+    assert func(text_input.value) == val3
+
+@pytest.mark.parametrize(
+    "widget",
+    [
+       EditableIntSlider,
+       EditableFloatSlider,
+    ],
+    ids=["EditableIntSlider", "EditableFloatSlider"]
+)
+def test_editableslider_button_end(page, port, widget):
+    widget = widget(step=1)
+    default_value = widget.value
+    step = widget.step
+    end = widget.end
+
+    serve(widget, port=port, threaded=True, show=False)
+    time.sleep(0.2)
+
+    page.goto(f"http://localhost:{port}")
+
+    up = page.locator("button").nth(0)
+    down = page.locator("button").nth(1)
+
+    up.click()
+    wait_until(page, lambda: widget.value == default_value + step)
+    assert widget.value == default_value + step
+    assert widget._slider.end == end
+
+    up.click()
+    wait_until(page, lambda: widget.value == default_value + 2 * step)
+    assert widget._slider.end == end + step
+
+    down.click()
+    wait_until(page, lambda: widget.value == default_value + step)
+    assert widget._slider.end == end + step
+
+
+@pytest.mark.parametrize(
+    "widget",
+    [
+       EditableIntSlider,
+       EditableFloatSlider
+    ],
+    ids=["EditableIntSlider", "EditableFloatSlider"]
+)
+def test_editableslider_button_start(page, port, widget):
+    widget = widget(step=1)
+    default_value = widget.value
+    step = widget.step
+    start = widget.start
+
+    serve(widget, port=port, threaded=True, show=False)
+    time.sleep(0.2)
+
+    page.goto(f"http://localhost:{port}")
+
+    up = page.locator("button").nth(0)
+    down = page.locator("button").nth(1)
+
+    down.click()
+    wait_until(page, lambda: widget.value == default_value - step)
+    assert widget._slider.start == start - step
+
+    down.click()
+    wait_until(page, lambda: widget.value == default_value - 2 * step)
+    assert widget._slider.start == start - 2 * step
+
+    up.click()
+    wait_until(page, lambda: widget.value == default_value - step)
+    assert widget._slider.start == start - 2 * step
+
+
+@pytest.mark.parametrize(
+    "widget,val1,val2,val3,func",
+    [
+       (EditableRangeSlider, 25.5, 24.5, 19.5, float),
+    ],
+    ids=["EditableRangeSlider"]
+)
+def test_editablerangeslider_textinput_end(page, port, widget, val1, val2, val3, func):
+    widget = widget()
+
+    serve(widget, port=port, threaded=True, show=False)
+    time.sleep(0.2)
+
+    page.goto(f"http://localhost:{port}")
+
+    text_input = _editable_text_input(page, nth=1)
+
+    text_input.value = val1
+    wait_until(page, lambda: widget.value == (0, val1))
+    assert widget._slider.end == val1
+    assert func(text_input.value) == val1
+
+    text_input.value = val2
+    wait_until(page, lambda: widget.value == (0, val2))
+    assert widget._slider.end == val1
+    assert func(text_input.value) == val2
+
+    # Setting fixed end
+    widget.fixed_end = val3
+    wait_until(page, lambda: widget.value == (0, val3))
+    assert widget._slider.end == val3
+    assert func(text_input.value) == val3
+
+
+@pytest.mark.parametrize(
+    "widget,val1,val2,val3,func",
+    [
+       (EditableRangeSlider, -25.5, -24.5, -19.5, float),
+    ],
+    ids=["EditableRangeSlider"]
+)
+def test_editablerangeslider_textinput_start(page, port, widget, val1, val2, val3, func):
+    widget = widget()
+
+    serve(widget, port=port, threaded=True, show=False)
+    time.sleep(0.2)
+
+    page.goto(f"http://localhost:{port}")
+
+    text_input = _editable_text_input(page, nth=0)
+
+    text_input.value = val1
+    wait_until(page, lambda: widget.value == (val1, 1))
+    assert widget._slider.start == val1
+    assert func(text_input.value) == val1
+
+    text_input.value = val2
+    wait_until(page, lambda: widget.value == (val2, 1))
+    assert widget._slider.start == val1
+    assert func(text_input.value) == val2
+
+    # Setting fixed start
+    widget.fixed_start = val3
+    wait_until(page, lambda: widget.value == (val3, 1))
+    assert widget._slider.start == val3
+    assert func(text_input.value) == val3
+
+
+@pytest.mark.parametrize(
+    "widget",
+    [
+       EditableRangeSlider,
+    ],
+    ids=["EditableRangeSlider"]
+)
+def test_editablerangeslider_button_end(page, port, widget):
+    widget = widget(step=1)
+    default_value = widget.value
+    step = widget.step
+    end = widget.end
+
+    serve(widget, port=port, threaded=True, show=False)
+    time.sleep(0.2)
+
+    page.goto(f"http://localhost:{port}")
+
+    up = page.locator("button").nth(2)
+    down = page.locator("button").nth(3)
+
+    up.click()
+    wait_until(page, lambda: widget.value == (0, default_value[1] + step))
+    assert widget._slider.end == end + step
+
+    up.click()
+    wait_until(page, lambda: widget.value == (0, default_value[1] + 2 * step))
+    assert widget._slider.end == end + 2 * step
+
+    down.click()
+    wait_until(page, lambda: widget.value == (0, default_value[1] + step))
+    assert widget._slider.end == end + 2 * step
+
+
+
+@pytest.mark.parametrize(
+    "widget",
+    [
+       EditableRangeSlider,
+    ],
+    ids=["EditableRangeSlider"]
+)
+def test_editablerangeslider_button_start(page, port, widget):
+    widget = widget(step=1)
+    default_value = widget.value
+    step = widget.step
+    start = widget.start
+
+    serve(widget, port=port, threaded=True, show=False)
+    time.sleep(0.2)
+
+    page.goto(f"http://localhost:{port}")
+
+    up = page.locator("button").nth(0)
+    down = page.locator("button").nth(1)
+
+    down.click()
+    wait_until(page, lambda: widget.value == (default_value[0] - step, 1))
+    assert widget._slider.start == start - step
+
+    down.click()
+    wait_until(page, lambda: widget.value == (default_value[0] - 2 * step, 1))
+    assert widget._slider.start == start - 2 * step
+
+    up.click()
+    wait_until(page, lambda: widget.value == (default_value[0] - step, 1))
+    assert widget._slider.start == start - 2 * step
+
+
+def test_editablerangeslider_no_overlap(page, port):
+    widget = EditableRangeSlider(value=(0, 2), step=1)
+
+    serve(widget, port=port, threaded=True, show=False)
+    time.sleep(0.2)
+
+    page.goto(f"http://localhost:{port}")
+
+    up_start = page.locator("button").nth(0)
+    down_start = page.locator("button").nth(1)
+    down_end = page.locator("button").nth(3)
+
+    up_start.click(click_count=3)
+    wait_until(page, lambda: widget.value == (2, 2))
+
+    down_start.click()
+    wait_until(page, lambda: widget.value == (1, 2))
+
+    down_end.click(click_count=3)
+    wait_until(page, lambda: widget.value == (1, 1))
diff --git a/panel/tests/widgets/test_slider.py b/panel/tests/widgets/test_slider.py
--- a/panel/tests/widgets/test_slider.py
+++ b/panel/tests/widgets/test_slider.py
@@ -10,7 +10,8 @@
 from panel import config
 from panel.widgets import (
     DateRangeSlider, DateSlider, DiscreteSlider, EditableFloatSlider,
-    EditableIntSlider, FloatSlider, IntSlider, RangeSlider, StaticText,
+    EditableIntSlider, EditableRangeSlider, FloatSlider, IntSlider,
+    RangeSlider, StaticText,
 )
 
 
@@ -405,15 +406,15 @@ def test_discrete_slider_options_dict(document, comm):
 
 
 @pytest.mark.parametrize(
-    'editableslider,start,end,step,val1,val2,val3',
+    'editableslider,start,end,step,val1,val2,val3,diff1',
     [
-        (EditableFloatSlider, 0.1, 0.5, 0.1, 0.4, 0.2, 0.5),
-        (EditableIntSlider, 1, 5, 1, 4, 2, 5)
+        (EditableFloatSlider, 0.1, 0.5, 0.1, 0.4, 0.2, 0.5, 0.1),
+        (EditableIntSlider, 1, 5, 1, 4, 2, 5, 1),
     ],
     ids=["EditableFloatSlider", "EditableIntSlider"]
 )
-def test_editable_float_slider(document, comm,
-    editableslider, start, end, step, val1, val2, val3):
+def test_editable_slider(document, comm,
+    editableslider, start, end, step, val1, val2, val3, diff1):
 
     slider = editableslider(start=start, end=end, value=val1, name='Slider')
 
@@ -464,3 +465,140 @@ def test_editable_float_slider(document, comm,
     slider.name = 'New Slider'
 
     assert static_widget.text == 'New Slider:'
+
+    # Testing update to fixed start
+    slider.fixed_start = slider.value + diff1
+    assert slider._slider.start == slider.fixed_start == slider_widget.start
+    slider.fixed_start = None
+
+    # Testing update to fixed end
+    slider.fixed_end = slider.value - diff1
+    assert slider._slider.end == slider.fixed_end == slider_widget.end
+    slider.fixed_end = None
+
+
+@pytest.mark.parametrize(
+    'editableslider,start,end,step,val1,val2,val3,diff1',
+    [
+        (EditableRangeSlider, 0.1, 0.5, 0.1, (0.2, 0.4), (0.2, 0.3), (0.1, 0.5), 0.1),
+    ],
+    ids=["EditableRangeSlider"]
+)
+def test_editable_rangeslider(document, comm,
+    editableslider, start, end, step, val1, val2, val3, diff1):
+
+    slider = editableslider(start=start, end=end, value=val1, name='Slider')
+
+    widget = slider.get_root(document, comm=comm)
+
+    assert isinstance(widget, BkColumn)
+
+    col_items = widget.children
+
+    assert len(col_items) == 2
+
+    row, slider_widget = col_items
+
+    assert slider_widget.title == ''
+    assert slider_widget.step == step
+    assert slider_widget.start == start
+    assert slider_widget.end == end
+    assert slider_widget.value == val1
+
+    assert isinstance(row, BkRow)
+
+    static_widget, input_widget_start, _, input_widget_end = row.children
+
+    assert input_widget_start.title == ''
+    assert input_widget_start.step == step
+    assert input_widget_start.value == val1[0]
+    assert input_widget_end.title == ''
+    assert input_widget_end.step == step
+    assert input_widget_end.value == val1[1]
+
+    slider._process_events({'value': val2})
+    assert slider.value == (input_widget_start.value, input_widget_end.value) == slider_widget.value == val2
+    slider._process_events({'value_throttled': val2})
+    assert slider.value_throttled == val2
+
+    # Testing throttled mode
+    with config.set(throttled=True):
+        slider._process_events({'value': val1})
+        assert slider.value == val2 # no change
+        slider._process_events({'value_throttled': val1})
+        assert slider.value == val1
+
+        slider.value = val3
+        assert (input_widget_start.value, input_widget_end.value) == slider_widget.value == val3
+
+    slider.name = 'New Slider'
+
+    assert static_widget.text == 'New Slider:'
+
+    # Testing update to fixed start
+    slider.fixed_start = slider.value[0] + diff1
+    assert slider._slider.start == slider.fixed_start == slider_widget.start
+    slider.fixed_start = None
+
+    # Testing update to fixed end
+    slider.fixed_end = slider.value[1] - diff1
+    assert slider._slider.end == slider.fixed_end == slider_widget.end
+    slider.fixed_end = None
+
+
+@pytest.mark.parametrize(
+    "editableslider",
+    [EditableFloatSlider, EditableIntSlider],
+)
+@pytest.mark.parametrize(
+    'start,end,fixed_start,fixed_end,val_init,val_update,fail_init,fail_update',
+    [
+        (1, 5, 0, None, 2, 3, False, False),
+        (1, 5, 0, None, 2, -1, False, True),
+        (1, 5, 0, None, -1, -1, True, True),
+        (1, 5, 0, None, 0, 100, False, False),
+    ],
+)
+def test_editable_slider_bounds(
+    editableslider,start,end,fixed_start,fixed_end,val_init,val_update,fail_init,fail_update):
+
+    try:
+        slider = editableslider(
+            start=start, end=end,
+            fixed_start=fixed_start, fixed_end=fixed_end,
+            value=val_init, name='Slider'
+        )
+    except Exception:
+        assert fail_init
+
+    try:
+        slider.value = val_update
+    except Exception:
+        assert fail_update
+
+
+@pytest.mark.parametrize(
+    'editableslider,start,end,fixed_start,fixed_end,val_init,val_update,fail_init,fail_update',
+    [
+        (EditableRangeSlider, 1, 5, 0, None, (2, 5), (3, 5), False, False),
+        (EditableRangeSlider, 1, 5, 0, None, (2, 5), (-1, 4), False, True),
+        (EditableRangeSlider, 1, 5, 0, None, (-1, 100), (-1, 200), True, True),
+        (EditableRangeSlider, 1, 5, 0, None, (1, 5), (0, 100), False, False),
+    ],
+)
+def test_editable_rangeslider_bounds(
+    editableslider,start,end,fixed_start,fixed_end,val_init,val_update,fail_init,fail_update):
+
+    try:
+        slider = editableslider(
+            start=start, end=end,
+            fixed_start=fixed_start, fixed_end=fixed_end,
+            value=val_init, name='Slider'
+        )
+    except Exception:
+        assert fail_init
+
+    try:
+        slider.value = val_update
+    except Exception:
+        assert fail_update