diff --git a/android/tests_backend/widgets/slider.py b/android/tests_backend/widgets/slider.py index fefa45c154..667d345168 100644 --- a/android/tests_backend/widgets/slider.py +++ b/android/tests_backend/widgets/slider.py @@ -15,6 +15,13 @@ def position(self): def change(self, position): self.native.setProgress(self._min + round(position * (self._max - self._min))) + @property + def tick_count(self): + # The Android backend does not currently display tick marks, so assume that it's + # in continuous mode if the range is very large. + range = self._max - self._min + 1 + return range if range < 10000 else None + @property def _min(self): return 0 if (Build.VERSION.SDK_INT < 26) else self.native.getMin() diff --git a/core/src/toga/widgets/slider.py b/core/src/toga/widgets/slider.py index 1e893502b7..cef91ce73b 100644 --- a/core/src/toga/widgets/slider.py +++ b/core/src/toga/widgets/slider.py @@ -187,6 +187,8 @@ def tick_count(self): @tick_count.setter def tick_count(self, tick_count): + if (tick_count is not None) and (tick_count < 2): + raise ValueError("Tick count must be at least 2") self._tick_count = tick_count self._impl.set_tick_count(tick_count) diff --git a/core/tests/widgets/test_slider.py b/core/tests/widgets/test_slider.py index 0464128d84..9de45c947c 100644 --- a/core/tests/widgets/test_slider.py +++ b/core/tests/widgets/test_slider.py @@ -30,10 +30,6 @@ def test_widget_created(self): self.assertEqual(self.slider._impl.interface, self.slider) self.assertActionPerformed(self.slider, "create Slider") - def test_get_value_invokes_impl_method(self): - self.slider.value - self.assertValueGet(self.slider, "value") - def test_set_value_between_min_and_max(self): value = 30 tick_value = 4 @@ -64,6 +60,26 @@ def test_set_value_to_be_too_big(self): self.slider.value = self.max_val + 1 self.assert_slider_value(tick_value=self.default_tick, value=self.value) + def test_set_tick_value_between_min_and_max(self): + value = 30 + tick_value = 4 + self.slider.tick_value = tick_value + self.assert_slider_value( + value=value, tick_value=tick_value, on_change_call_count=1 + ) + + def test_set_tick_value_to_be_min(self): + self.slider.tick_value = 1 + self.assert_slider_value( + value=self.min_val, tick_value=1, on_change_call_count=1 + ) + + def test_set_tick_value_to_be_max(self): + self.slider.tick_value = self.tick_count + self.assert_slider_value( + value=self.max_val, tick_value=self.tick_count, on_change_call_count=1 + ) + def test_set_tick_value_to_be_too_small(self): with self.assertRaises(ValueError): self.slider.tick_value = 0 @@ -71,9 +87,16 @@ def test_set_tick_value_to_be_too_small(self): def test_set_tick_value_to_be_too_big(self): with self.assertRaises(ValueError): - self.slider.tick_value = self.max_val + 1 + self.slider.tick_value = self.tick_count + 1 self.assert_slider_value(tick_value=self.default_tick, value=self.value) + def test_tick_value_without_tick_count(self): + self.slider.tick_count = None + with self.assertRaisesRegex( + ValueError, "Cannot set tick value when tick count is None" + ): + self.slider.tick_value = 4 + def test_new_value_is_None(self): self.slider.value = None self.assertEqual(self.slider.value, 50) @@ -122,9 +145,12 @@ def test_working_range_values(self): self.assert_set_range(0, 100) self.assert_set_range(100, 1000) - def test_false_range(self): - with self.assertRaises(ValueError): - self.slider.range = (100, 0) + def test_invalid_range_values(self): + for range in [(0, 0), (100, 0)]: + with self.assertRaisesRegex( + ValueError, "Range min value has to be smaller than max value." + ): + self.slider.range = range def test_set_enabled_with_working_values(self): self.assertEqual(self.slider.enabled, self.enabled) @@ -136,9 +162,24 @@ def test_get_tick_count(self): self.assertEqual(self.tick_count, tick_count) def test_set_tick_count(self): - new_tick_count = 5 - self.slider.tick_count = new_tick_count - self.assertValueSet(self.slider, "tick_count", new_tick_count) + self.slider.range = (10, 110) + for tick_count, tick_step, tick_value in [ + (None, None, None), + (11, 10, 5), # Exactly 50 + (5, 25, 3), # Round up to 60 + (4, 100 / 3, 2), # Round down to 43.333 + (2, 100, 1), # Round down to 10 (2 is the minimum possible tick_count) + ]: + self.slider.tick_count = tick_count + self.assertEqual(self.slider.tick_count, tick_count) + self.assertValueSet(self.slider, "tick_count", tick_count) + self.assertEqual(self.slider.tick_step, tick_step) + self.assert_slider_value(tick_value=tick_value, value=self.value) + + def test_set_tick_count_too_small(self): + for tick_count in [1, 0, -1]: + with self.assertRaisesRegex(ValueError, "Tick count must be at least 2"): + self.slider.tick_count = tick_count def test_focus(self): self.slider.focus() diff --git a/testbed/tests/widgets/test_slider.py b/testbed/tests/widgets/test_slider.py index 538a8578e5..57c011fa91 100644 --- a/testbed/tests/widgets/test_slider.py +++ b/testbed/tests/widgets/test_slider.py @@ -34,6 +34,7 @@ async def test_init(widget, probe, on_change): on_change.assert_not_called() +# Bounds checks are covered by core tests. async def test_value(widget, probe, on_change): for scale in SCALES: widget.range = (0, scale) @@ -54,12 +55,11 @@ async def test_change(widget, probe, on_change): on_change.assert_called_once_with(widget) +# Bounds checks and the `min` property are covered by the core tests. async def test_min(widget, probe, on_change): for min in POSITIONS[:-1]: on_change.reset_mock() - range = (min, 1) - assert_set_get(widget, "range", range) - assert (widget.min, widget.max) == range + assert_set_get(widget, "range", (min, 1)) if min <= 0.5: # The existing value is in the range, so it should not change. @@ -73,13 +73,12 @@ async def test_min(widget, probe, on_change): on_change.assert_called_once_with(widget) +# Bounds checks and the `max` property are covered by the core tests. async def test_max(widget, probe, on_change): # If the existing value is in the range, it should not change. for max in POSITIONS[-1:0:-1]: on_change.reset_mock() - range = (0, max) - assert_set_get(widget, "range", range) - assert (widget.min, widget.max) == range + assert_set_get(widget, "range", (0, max)) if max >= 0.5: # The existing value is in the range, so it should not change. @@ -91,3 +90,10 @@ async def test_max(widget, probe, on_change): assert widget.value == max assert probe.position == 1 on_change.assert_called_once_with(widget) + + +# All other tick functionality is covered by the core tests. +async def test_ticks(widget, probe): + for tick_count in [2, None, 10]: + widget.tick_count = tick_count + assert probe.tick_count == tick_count diff --git a/winforms/tests_backend/widgets/slider.py b/winforms/tests_backend/widgets/slider.py index c1258159d8..d6fb9af3da 100644 --- a/winforms/tests_backend/widgets/slider.py +++ b/winforms/tests_backend/widgets/slider.py @@ -1,10 +1,10 @@ -import System.Windows.Forms +from System.Windows.Forms import TickStyle, TrackBar from .base import SimpleProbe class SliderProbe(SimpleProbe): - native_class = System.Windows.Forms.TrackBar + native_class = TrackBar @property def position(self): @@ -13,6 +13,16 @@ def position(self): def change(self, position): self.native.Value = self._min + round(position * (self._max - self._min)) + @property + def tick_count(self): + tick_style = self.native.TickStyle + if tick_style == TickStyle.BottomRight: + return self._max - self._min + 1 + elif tick_style == getattr(TickStyle, "None"): + return None + else: + raise ValueError(f"unknown TickStyle {tick_style}") + @property def _min(self): return self.native.Minimum