You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
844 lines
32 KiB
844 lines
32 KiB
|
6 months ago
|
import pytest
|
||
|
|
import datetime
|
||
|
|
import calendar
|
||
|
|
from unittest.mock import Mock, patch, MagicMock
|
||
|
|
|
||
|
|
from TermTk.TTkCore.constant import TTkK
|
||
|
|
from TermTk.TTkCore.TTkTerm.inputkey import TTkKeyEvent
|
||
|
|
from TermTk.TTkCore.TTkTerm.inputmouse import TTkMouseEvent
|
||
|
|
from TermTk.TTkWidgets.datetime_date import TTkDate, _FieldSelected, _TTkTimeWidgetState
|
||
|
|
|
||
|
|
|
||
|
|
class TestTTkDateFieldSelected:
|
||
|
|
"""Test _FieldSelected enum values."""
|
||
|
|
|
||
|
|
def test_field_selected_values(self):
|
||
|
|
"""Test that _FieldSelected enum has expected values."""
|
||
|
|
assert _FieldSelected.NONE == 1
|
||
|
|
assert _FieldSelected.YEARS == 2
|
||
|
|
assert _FieldSelected.MONTHS == 3
|
||
|
|
assert _FieldSelected.DAYS == 4
|
||
|
|
assert _FieldSelected.CAL == 5
|
||
|
|
|
||
|
|
|
||
|
|
class TestTTkTimeWidgetState:
|
||
|
|
"""Test _TTkTimeWidgetState dataclass."""
|
||
|
|
|
||
|
|
def setup_method(self):
|
||
|
|
"""Set up test fixtures."""
|
||
|
|
self.state = _TTkTimeWidgetState()
|
||
|
|
|
||
|
|
def test_default_state(self):
|
||
|
|
"""Test default state initialization."""
|
||
|
|
assert self.state.selected == _FieldSelected.NONE
|
||
|
|
assert self.state.hovered == _FieldSelected.NONE
|
||
|
|
assert self.state.digit == 0
|
||
|
|
|
||
|
|
def test_clear_state(self):
|
||
|
|
"""Test state clearing functionality."""
|
||
|
|
# Set some values
|
||
|
|
self.state.selected = _FieldSelected.YEARS
|
||
|
|
self.state.hovered = _FieldSelected.MONTHS
|
||
|
|
self.state.digit = 2
|
||
|
|
|
||
|
|
# Clear and verify
|
||
|
|
self.state.clear()
|
||
|
|
assert self.state.selected == _FieldSelected.NONE
|
||
|
|
assert self.state.hovered == _FieldSelected.NONE
|
||
|
|
assert self.state.digit == 0
|
||
|
|
|
||
|
|
|
||
|
|
class TestTTkDateStaticMethods:
|
||
|
|
"""Test static methods of TTkDate."""
|
||
|
|
|
||
|
|
def test_get_field_from_pos_years(self):
|
||
|
|
"""Test field detection for years position."""
|
||
|
|
# Years field spans positions 0-3
|
||
|
|
assert TTkDate._getFieldFromPos(0, 0) == _FieldSelected.YEARS
|
||
|
|
assert TTkDate._getFieldFromPos(3, 0) == _FieldSelected.YEARS
|
||
|
|
|
||
|
|
def test_get_field_from_pos_months(self):
|
||
|
|
"""Test field detection for months position."""
|
||
|
|
# Months field spans positions 5-6
|
||
|
|
assert TTkDate._getFieldFromPos(5, 0) == _FieldSelected.MONTHS
|
||
|
|
assert TTkDate._getFieldFromPos(6, 0) == _FieldSelected.MONTHS
|
||
|
|
|
||
|
|
def test_get_field_from_pos_days(self):
|
||
|
|
"""Test field detection for days position."""
|
||
|
|
# Days field spans positions 8-9
|
||
|
|
assert TTkDate._getFieldFromPos(8, 0) == _FieldSelected.DAYS
|
||
|
|
assert TTkDate._getFieldFromPos(9, 0) == _FieldSelected.DAYS
|
||
|
|
|
||
|
|
def test_get_field_from_pos_calendar(self):
|
||
|
|
"""Test field detection for calendar button position."""
|
||
|
|
# Calendar button spans positions 11-12
|
||
|
|
assert TTkDate._getFieldFromPos(11, 0) == _FieldSelected.CAL
|
||
|
|
assert TTkDate._getFieldFromPos(12, 0) == _FieldSelected.CAL
|
||
|
|
|
||
|
|
def test_get_field_from_pos_separators(self):
|
||
|
|
"""Test field detection for separator positions."""
|
||
|
|
# Separators should return NONE
|
||
|
|
assert TTkDate._getFieldFromPos(4, 0) == _FieldSelected.NONE # First separator
|
||
|
|
assert TTkDate._getFieldFromPos(7, 0) == _FieldSelected.NONE # Second separator
|
||
|
|
assert TTkDate._getFieldFromPos(10, 0) == _FieldSelected.NONE # Space before calendar
|
||
|
|
|
||
|
|
def test_get_field_from_pos_wrong_row(self):
|
||
|
|
"""Test field detection returns NONE for non-zero rows."""
|
||
|
|
assert TTkDate._getFieldFromPos(0, 1) == _FieldSelected.NONE
|
||
|
|
assert TTkDate._getFieldFromPos(5, -1) == _FieldSelected.NONE
|
||
|
|
|
||
|
|
def test_get_field_from_pos_out_of_bounds(self):
|
||
|
|
"""Test field detection for out-of-bounds positions."""
|
||
|
|
assert TTkDate._getFieldFromPos(-1, 0) == _FieldSelected.NONE
|
||
|
|
assert TTkDate._getFieldFromPos(13, 0) == _FieldSelected.NONE
|
||
|
|
|
||
|
|
|
||
|
|
class TestTTkDateInit:
|
||
|
|
"""Test TTkDate initialization."""
|
||
|
|
|
||
|
|
def test_init_default_date(self):
|
||
|
|
"""Test initialization with default (today's) date."""
|
||
|
|
widget = TTkDate()
|
||
|
|
today = datetime.date.today()
|
||
|
|
assert widget.date() == today
|
||
|
|
|
||
|
|
def test_init_with_date(self):
|
||
|
|
"""Test initialization with specific date."""
|
||
|
|
test_date = datetime.date(2023, 6, 15)
|
||
|
|
widget = TTkDate(date=test_date)
|
||
|
|
assert widget.date() == test_date
|
||
|
|
|
||
|
|
def test_init_signal_creation(self):
|
||
|
|
"""Test that dateChanged signal is created."""
|
||
|
|
widget = TTkDate()
|
||
|
|
assert hasattr(widget, 'dateChanged')
|
||
|
|
|
||
|
|
def test_init_ordinal_bounds(self):
|
||
|
|
"""Test that ordinal bounds are set correctly."""
|
||
|
|
widget = TTkDate()
|
||
|
|
expected_min = datetime.date(1900, 1, 1).toordinal()
|
||
|
|
expected_max = datetime.date(2100, 12, 31).toordinal()
|
||
|
|
assert widget._minOrdinal == expected_min
|
||
|
|
assert widget._maxOrdinal == expected_max
|
||
|
|
|
||
|
|
def test_init_focus_policy(self):
|
||
|
|
"""Test that focus policy is set correctly."""
|
||
|
|
widget = TTkDate()
|
||
|
|
assert widget.focusPolicy() & TTkK.ClickFocus
|
||
|
|
assert widget.focusPolicy() & TTkK.TabFocus
|
||
|
|
|
||
|
|
|
||
|
|
class TestTTkDateCore:
|
||
|
|
"""Test core TTkDate functionality."""
|
||
|
|
|
||
|
|
def setup_method(self):
|
||
|
|
"""Set up test fixtures."""
|
||
|
|
self.test_date = datetime.date(2023, 6, 15)
|
||
|
|
self.widget = TTkDate(date=self.test_date)
|
||
|
|
|
||
|
|
def test_date_getter(self):
|
||
|
|
"""Test date() getter method."""
|
||
|
|
assert self.widget.date() == self.test_date
|
||
|
|
|
||
|
|
def test_set_date_updates_internal_date(self):
|
||
|
|
"""Test setDate() updates internal date."""
|
||
|
|
new_date = datetime.date(2024, 12, 25)
|
||
|
|
self.widget.setDate(new_date)
|
||
|
|
assert self.widget.date() == new_date
|
||
|
|
|
||
|
|
@patch.object(TTkDate, 'update')
|
||
|
|
def test_set_date_calls_update(self, mock_update):
|
||
|
|
"""Test setDate() calls update() when date changes."""
|
||
|
|
new_date = datetime.date(2024, 1, 1)
|
||
|
|
self.widget.setDate(new_date)
|
||
|
|
mock_update.assert_called_once()
|
||
|
|
|
||
|
|
def test_set_date_emits_signal(self):
|
||
|
|
"""Test setDate() emits dateChanged signal."""
|
||
|
|
signal_received = []
|
||
|
|
|
||
|
|
def slot(date_value):
|
||
|
|
signal_received.append(date_value)
|
||
|
|
|
||
|
|
self.widget.dateChanged.connect(slot)
|
||
|
|
new_date = datetime.date(2024, 3, 10)
|
||
|
|
self.widget.setDate(new_date)
|
||
|
|
|
||
|
|
assert len(signal_received) == 1
|
||
|
|
assert signal_received[0] == new_date
|
||
|
|
|
||
|
|
def test_set_date_same_date_no_signal(self):
|
||
|
|
"""Test setDate() with same date doesn't emit signal."""
|
||
|
|
signal_received = []
|
||
|
|
|
||
|
|
def slot(date_value):
|
||
|
|
signal_received.append(date_value)
|
||
|
|
|
||
|
|
self.widget.dateChanged.connect(slot)
|
||
|
|
self.widget.setDate(self.test_date) # Same date
|
||
|
|
|
||
|
|
assert len(signal_received) == 0
|
||
|
|
|
||
|
|
def test_add_delta_positive(self):
|
||
|
|
"""Test _addDelta() with positive delta."""
|
||
|
|
initial_ordinal = self.test_date.toordinal()
|
||
|
|
self.widget._addDelta(10)
|
||
|
|
expected_date = datetime.date.fromordinal(initial_ordinal + 10)
|
||
|
|
assert self.widget.date() == expected_date
|
||
|
|
|
||
|
|
def test_add_delta_negative(self):
|
||
|
|
"""Test _addDelta() with negative delta."""
|
||
|
|
initial_ordinal = self.test_date.toordinal()
|
||
|
|
self.widget._addDelta(-5)
|
||
|
|
expected_date = datetime.date.fromordinal(initial_ordinal - 5)
|
||
|
|
assert self.widget.date() == expected_date
|
||
|
|
|
||
|
|
def test_add_delta_zero(self):
|
||
|
|
"""Test _addDelta() with zero delta doesn't change date."""
|
||
|
|
initial_date = self.widget.date()
|
||
|
|
self.widget._addDelta(0)
|
||
|
|
assert self.widget.date() == initial_date
|
||
|
|
|
||
|
|
def test_add_delta_min_bound(self):
|
||
|
|
"""Test _addDelta() respects minimum date bound."""
|
||
|
|
# Set date near minimum bound
|
||
|
|
min_date = datetime.date(1900, 1, 5)
|
||
|
|
self.widget.setDate(min_date)
|
||
|
|
|
||
|
|
# Try to go below minimum
|
||
|
|
self.widget._addDelta(-10)
|
||
|
|
|
||
|
|
# Should be clamped to minimum
|
||
|
|
expected_min = datetime.date.fromordinal(self.widget._minOrdinal)
|
||
|
|
assert self.widget.date() == expected_min
|
||
|
|
|
||
|
|
def test_add_delta_max_bound(self):
|
||
|
|
"""Test _addDelta() respects maximum date bound."""
|
||
|
|
# Set date near maximum bound
|
||
|
|
max_date = datetime.date(2100, 12, 25)
|
||
|
|
self.widget.setDate(max_date)
|
||
|
|
|
||
|
|
# Try to go above maximum
|
||
|
|
self.widget._addDelta(10)
|
||
|
|
|
||
|
|
# Should be clamped to maximum
|
||
|
|
expected_max = datetime.date.fromordinal(self.widget._maxOrdinal)
|
||
|
|
assert self.widget.date() == expected_max
|
||
|
|
|
||
|
|
def test_focus_out_event_clears_state(self):
|
||
|
|
"""Test focusOutEvent() clears widget state."""
|
||
|
|
# Set some state
|
||
|
|
self.widget._state.selected = _FieldSelected.YEARS
|
||
|
|
self.widget._state.hovered = _FieldSelected.MONTHS
|
||
|
|
self.widget._state.digit = 2
|
||
|
|
|
||
|
|
# Focus out
|
||
|
|
self.widget.focusOutEvent()
|
||
|
|
|
||
|
|
# Verify state is cleared
|
||
|
|
assert self.widget._state.selected == _FieldSelected.NONE
|
||
|
|
assert self.widget._state.hovered == _FieldSelected.NONE
|
||
|
|
assert self.widget._state.digit == 0
|
||
|
|
|
||
|
|
|
||
|
|
class TestTTkDateMouseEvents:
|
||
|
|
"""Test mouse event handling."""
|
||
|
|
|
||
|
|
def setup_method(self):
|
||
|
|
"""Set up test fixtures."""
|
||
|
|
self.widget = TTkDate(date=datetime.date(2023, 6, 15))
|
||
|
|
|
||
|
|
@patch.object(TTkDate, 'update')
|
||
|
|
def test_mouse_press_event_selects_field(self, mock_update):
|
||
|
|
"""Test mousePressEvent() selects correct field."""
|
||
|
|
evt = TTkMouseEvent(0, 0, TTkMouseEvent.Press, TTkMouseEvent.LeftButton, TTkK.NoModifier, 1, '')
|
||
|
|
result = self.widget.mousePressEvent(evt)
|
||
|
|
|
||
|
|
assert result is True
|
||
|
|
assert self.widget._state.selected == _FieldSelected.YEARS
|
||
|
|
mock_update.assert_called_once()
|
||
|
|
|
||
|
|
def test_mouse_press_event_clears_state(self):
|
||
|
|
"""Test mousePressEvent() clears existing state."""
|
||
|
|
# Set some state first
|
||
|
|
self.widget._state.hovered = _FieldSelected.DAYS
|
||
|
|
self.widget._state.digit = 3
|
||
|
|
|
||
|
|
evt = TTkMouseEvent(5, 0, TTkMouseEvent.Press, TTkMouseEvent.LeftButton, TTkK.NoModifier, 1, '')
|
||
|
|
self.widget.mousePressEvent(evt)
|
||
|
|
|
||
|
|
assert self.widget._state.selected == _FieldSelected.MONTHS
|
||
|
|
assert self.widget._state.digit == 0
|
||
|
|
|
||
|
|
@patch.object(TTkDate, '_showForm')
|
||
|
|
def test_mouse_press_calendar_shows_form(self, mock_show_form):
|
||
|
|
"""Test clicking calendar button shows form."""
|
||
|
|
evt = TTkMouseEvent(11, 0, TTkMouseEvent.Press, TTkMouseEvent.LeftButton, TTkK.NoModifier, 1, '')
|
||
|
|
self.widget.mousePressEvent(evt)
|
||
|
|
|
||
|
|
assert self.widget._state.selected == _FieldSelected.CAL
|
||
|
|
mock_show_form.assert_called_once()
|
||
|
|
|
||
|
|
@patch.object(TTkDate, 'update')
|
||
|
|
def test_mouse_move_event_sets_hover(self, mock_update):
|
||
|
|
"""Test mouseMoveEvent() sets hover state."""
|
||
|
|
evt = TTkMouseEvent(8, 0, TTkK.Move, TTkK.NoButton, TTkK.NoModifier, 1, '')
|
||
|
|
result = self.widget.mouseMoveEvent(evt)
|
||
|
|
|
||
|
|
assert result is True
|
||
|
|
assert self.widget._state.hovered == _FieldSelected.DAYS
|
||
|
|
mock_update.assert_called_once()
|
||
|
|
|
||
|
|
|
||
|
|
class TestTTkDateKeyEvents:
|
||
|
|
"""Test keyboard event handling."""
|
||
|
|
|
||
|
|
def setup_method(self):
|
||
|
|
"""Set up test fixtures."""
|
||
|
|
self.widget = TTkDate(date=datetime.date(2023, 6, 15))
|
||
|
|
|
||
|
|
def test_tab_navigation_forward(self):
|
||
|
|
"""Test forward Tab navigation through fields."""
|
||
|
|
# Start with NONE, tab to YEARS
|
||
|
|
self.widget._state.selected = _FieldSelected.NONE
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Tab, "", TTkK.NoModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
assert result is True
|
||
|
|
assert self.widget._state.selected == _FieldSelected.YEARS
|
||
|
|
|
||
|
|
# From YEARS to MONTHS
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Tab, "", TTkK.NoModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
assert result is True
|
||
|
|
assert self.widget._state.selected == _FieldSelected.MONTHS
|
||
|
|
|
||
|
|
# From MONTHS to DAYS
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Tab, "", TTkK.NoModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
assert result is True
|
||
|
|
assert self.widget._state.selected == _FieldSelected.DAYS
|
||
|
|
|
||
|
|
# From DAYS to CAL
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Tab, "", TTkK.NoModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
assert result is True
|
||
|
|
assert self.widget._state.selected == _FieldSelected.CAL
|
||
|
|
|
||
|
|
# From CAL should return False
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Tab, "", TTkK.NoModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
assert result is False
|
||
|
|
|
||
|
|
def test_tab_navigation_backward(self):
|
||
|
|
"""Test backward Tab navigation (Shift+Tab)."""
|
||
|
|
# Start with NONE, shift+tab to CAL
|
||
|
|
self.widget._state.selected = _FieldSelected.NONE
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Tab, "", TTkK.ShiftModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
assert result is True
|
||
|
|
assert self.widget._state.selected == _FieldSelected.CAL
|
||
|
|
|
||
|
|
# From CAL to DAYS
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Tab, "", TTkK.ShiftModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
assert result is True
|
||
|
|
assert self.widget._state.selected == _FieldSelected.DAYS
|
||
|
|
|
||
|
|
# From DAYS to MONTHS
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Tab, "", TTkK.ShiftModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
assert result is True
|
||
|
|
assert self.widget._state.selected == _FieldSelected.MONTHS
|
||
|
|
|
||
|
|
# From MONTHS to YEARS
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Tab, "", TTkK.ShiftModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
assert result is True
|
||
|
|
assert self.widget._state.selected == _FieldSelected.YEARS
|
||
|
|
|
||
|
|
# From YEARS should return False
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Tab, "", TTkK.ShiftModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
assert result is False
|
||
|
|
|
||
|
|
def test_arrow_navigation_right(self):
|
||
|
|
"""Test right arrow navigation."""
|
||
|
|
self.widget._state.selected = _FieldSelected.YEARS
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Right, "", TTkK.NoModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
assert result is True
|
||
|
|
assert self.widget._state.selected == _FieldSelected.MONTHS
|
||
|
|
|
||
|
|
def test_arrow_navigation_left(self):
|
||
|
|
"""Test left arrow navigation."""
|
||
|
|
self.widget._state.selected = _FieldSelected.MONTHS
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Left, "", TTkK.NoModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
assert result is True
|
||
|
|
assert self.widget._state.selected == _FieldSelected.YEARS
|
||
|
|
|
||
|
|
@patch.object(TTkDate, '_showForm')
|
||
|
|
def test_calendar_field_special_keys_show_form(self, mock_show_form):
|
||
|
|
"""Test special keys in calendar field show form."""
|
||
|
|
self.widget._state.selected = _FieldSelected.CAL
|
||
|
|
|
||
|
|
for key in (TTkK.Key_Up, TTkK.Key_Down, TTkK.Key_Enter):
|
||
|
|
mock_show_form.reset_mock()
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, key, "", TTkK.NoModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
assert result is True
|
||
|
|
mock_show_form.assert_called_once()
|
||
|
|
|
||
|
|
def test_up_arrow_increment_years(self):
|
||
|
|
"""Test up arrow increments years."""
|
||
|
|
initial_date = self.widget.date()
|
||
|
|
self.widget._state.selected = _FieldSelected.YEARS
|
||
|
|
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Up, "", TTkK.NoModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
assert result is True
|
||
|
|
new_date = self.widget.date()
|
||
|
|
# Year increment should add 365 or 366 days depending on leap year
|
||
|
|
days_in_year = 366 if calendar.isleap(initial_date.year) else 365
|
||
|
|
expected_ordinal = initial_date.toordinal() + days_in_year
|
||
|
|
assert new_date.toordinal() == expected_ordinal
|
||
|
|
|
||
|
|
def test_up_arrow_increment_months(self):
|
||
|
|
"""Test up arrow increments months (adds ~30 days)."""
|
||
|
|
initial_date = self.widget.date()
|
||
|
|
self.widget._state.selected = _FieldSelected.MONTHS
|
||
|
|
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Up, "", TTkK.NoModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
assert result is True
|
||
|
|
new_date = self.widget.date()
|
||
|
|
expected_ordinal = initial_date.toordinal() + 30
|
||
|
|
assert new_date.toordinal() == expected_ordinal
|
||
|
|
|
||
|
|
def test_up_arrow_increment_days(self):
|
||
|
|
"""Test up arrow increments days."""
|
||
|
|
initial_date = self.widget.date()
|
||
|
|
self.widget._state.selected = _FieldSelected.DAYS
|
||
|
|
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Up, "", TTkK.NoModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
assert result is True
|
||
|
|
new_date = self.widget.date()
|
||
|
|
expected_ordinal = initial_date.toordinal() + 1
|
||
|
|
assert new_date.toordinal() == expected_ordinal
|
||
|
|
|
||
|
|
def test_down_arrow_decrement_years(self):
|
||
|
|
"""Test down arrow decrements years."""
|
||
|
|
initial_date = self.widget.date()
|
||
|
|
self.widget._state.selected = _FieldSelected.YEARS
|
||
|
|
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Down, "", TTkK.NoModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
assert result is True
|
||
|
|
new_date = self.widget.date()
|
||
|
|
days_in_year = 366 if calendar.isleap(initial_date.year) else 365
|
||
|
|
expected_ordinal = initial_date.toordinal() - days_in_year
|
||
|
|
assert new_date.toordinal() == expected_ordinal
|
||
|
|
|
||
|
|
def test_delete_clears_years(self):
|
||
|
|
"""Test Delete key clears years field."""
|
||
|
|
self.widget._state.selected = _FieldSelected.YEARS
|
||
|
|
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Delete, "", TTkK.NoModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
assert result is True
|
||
|
|
assert self.widget.date().year == 1900
|
||
|
|
|
||
|
|
def test_delete_clears_months(self):
|
||
|
|
"""Test Delete key clears months field."""
|
||
|
|
self.widget._state.selected = _FieldSelected.MONTHS
|
||
|
|
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Delete, "", TTkK.NoModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
assert result is True
|
||
|
|
assert self.widget.date().month == 1
|
||
|
|
|
||
|
|
def test_delete_clears_days(self):
|
||
|
|
"""Test Delete key clears days field."""
|
||
|
|
self.widget._state.selected = _FieldSelected.DAYS
|
||
|
|
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Delete, "", TTkK.NoModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
assert result is True
|
||
|
|
assert self.widget.date().day == 1
|
||
|
|
|
||
|
|
def test_backspace_clears_fields(self):
|
||
|
|
"""Test Backspace key clears fields same as Delete."""
|
||
|
|
self.widget._state.selected = _FieldSelected.YEARS
|
||
|
|
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Backspace, "", TTkK.NoModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
assert result is True
|
||
|
|
assert self.widget.date().year == 1900
|
||
|
|
|
||
|
|
def test_enter_deselects_field(self):
|
||
|
|
"""Test Enter key deselects current field."""
|
||
|
|
self.widget._state.selected = _FieldSelected.YEARS
|
||
|
|
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Enter, "", TTkK.NoModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
assert result is True
|
||
|
|
assert self.widget._state.selected == _FieldSelected.NONE
|
||
|
|
|
||
|
|
def test_digit_input_years_first_digit(self):
|
||
|
|
"""Test entering first digit in years field."""
|
||
|
|
self.widget._state.selected = _FieldSelected.YEARS
|
||
|
|
self.widget._state.digit = 0
|
||
|
|
|
||
|
|
evt = TTkKeyEvent(TTkK.Character, "2", "", TTkK.NoModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
assert result is True
|
||
|
|
assert self.widget.date().year == 2000 # 2 * 10^3
|
||
|
|
assert self.widget._state.digit == 1
|
||
|
|
|
||
|
|
def test_digit_input_years_four_digits(self):
|
||
|
|
"""Test entering all four digits in years field."""
|
||
|
|
self.widget._state.selected = _FieldSelected.YEARS
|
||
|
|
self.widget._state.digit = 0
|
||
|
|
|
||
|
|
# Enter 2024
|
||
|
|
for i, digit in enumerate("2024"):
|
||
|
|
evt = TTkKeyEvent(TTkK.Character, digit, "", TTkK.NoModifier)
|
||
|
|
self.widget.keyEvent(evt)
|
||
|
|
assert self.widget._state.digit == (i + 1) % 4
|
||
|
|
|
||
|
|
assert self.widget.date().year == 2024
|
||
|
|
|
||
|
|
def test_digit_input_years_bounds(self):
|
||
|
|
"""Test years digit input respects bounds (1900-2100)."""
|
||
|
|
self.widget._state.selected = _FieldSelected.YEARS
|
||
|
|
self.widget._state.digit = 0
|
||
|
|
|
||
|
|
# Try to enter 1899 (below minimum)
|
||
|
|
for digit in "1899":
|
||
|
|
evt = TTkKeyEvent(TTkK.Character, digit, "", TTkK.NoModifier)
|
||
|
|
self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
assert self.widget.date().year == 1999 # Should be clamped to minimum
|
||
|
|
|
||
|
|
# Reset and try 2101 (above maximum)
|
||
|
|
self.widget._state.digit = 0
|
||
|
|
for digit in "2101":
|
||
|
|
evt = TTkKeyEvent(TTkK.Character, digit, "", TTkK.NoModifier)
|
||
|
|
self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
assert self.widget.date().year == 2100 # Should be clamped to maximum
|
||
|
|
|
||
|
|
def test_digit_input_months_first_digit(self):
|
||
|
|
"""Test entering first digit in months field."""
|
||
|
|
self.widget._state.selected = _FieldSelected.MONTHS
|
||
|
|
self.widget._state.digit = 0
|
||
|
|
|
||
|
|
evt = TTkKeyEvent(TTkK.Character, "1", "", TTkK.NoModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
assert result is True
|
||
|
|
assert self.widget.date().month == 1
|
||
|
|
assert self.widget._state.digit == 1
|
||
|
|
|
||
|
|
def test_digit_input_months_two_digits(self):
|
||
|
|
"""Test entering two digits in months field."""
|
||
|
|
self.widget._state.selected = _FieldSelected.MONTHS
|
||
|
|
self.widget._state.digit = 0
|
||
|
|
|
||
|
|
# Enter 12
|
||
|
|
evt = TTkKeyEvent(TTkK.Character, "1", "", TTkK.NoModifier)
|
||
|
|
self.widget.keyEvent(evt)
|
||
|
|
evt = TTkKeyEvent(TTkK.Character, "2", "", TTkK.NoModifier)
|
||
|
|
self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
assert self.widget.date().month == 12
|
||
|
|
assert self.widget._state.digit == 0 # Should wrap to 0
|
||
|
|
|
||
|
|
def test_digit_input_months_bounds(self):
|
||
|
|
"""Test months digit input respects bounds (1-12)."""
|
||
|
|
self.widget._state.selected = _FieldSelected.MONTHS
|
||
|
|
self.widget._state.digit = 1 # Second digit
|
||
|
|
|
||
|
|
# Try to enter 13 (month 1 + digit 3 = 13, should clamp to 12)
|
||
|
|
self.widget.setDate(datetime.date(2023, 1, 15)) # Set month to 1
|
||
|
|
evt = TTkKeyEvent(TTkK.Character, "3", "", TTkK.NoModifier)
|
||
|
|
self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
assert self.widget.date().month == 12 # Should be clamped
|
||
|
|
|
||
|
|
def test_digit_input_days_bounds(self):
|
||
|
|
"""Test days digit input respects month-specific bounds."""
|
||
|
|
# Test February in non-leap year
|
||
|
|
self.widget.setDate(datetime.date(2023, 2, 1)) # February 2023 (28 days)
|
||
|
|
self.widget._state.selected = _FieldSelected.DAYS
|
||
|
|
self.widget._state.digit = 0 # Second digit
|
||
|
|
|
||
|
|
# Try to enter 29 (day 2 + digit 9 = 29, should clamp to 28)
|
||
|
|
evt = TTkKeyEvent(TTkK.Character, "2", "", TTkK.NoModifier)
|
||
|
|
self.widget.keyEvent(evt)
|
||
|
|
evt = TTkKeyEvent(TTkK.Character, "9", "", TTkK.NoModifier)
|
||
|
|
self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
assert self.widget.date().day == 28 # Should be clamped to February limit
|
||
|
|
|
||
|
|
def test_digit_input_days_leap_year(self):
|
||
|
|
"""Test days input in leap year February."""
|
||
|
|
# Test February in leap year
|
||
|
|
self.widget.setDate(datetime.date(2024, 2, 1)) # February 2024 (leap year, 29 days)
|
||
|
|
self.widget._state.selected = _FieldSelected.DAYS
|
||
|
|
self.widget._state.digit = 0 # Second digit
|
||
|
|
|
||
|
|
# Enter 29
|
||
|
|
evt = TTkKeyEvent(TTkK.Character, "2", "", TTkK.NoModifier)
|
||
|
|
self.widget.keyEvent(evt)
|
||
|
|
evt = TTkKeyEvent(TTkK.Character, "9", "", TTkK.NoModifier)
|
||
|
|
self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
assert self.widget.date().day == 29 # Should be allowed in leap year
|
||
|
|
|
||
|
|
@patch.object(TTkDate, '_showForm')
|
||
|
|
def test_character_input_calendar_field_shows_form(self, mock_show_form):
|
||
|
|
"""Test character input in calendar field shows form."""
|
||
|
|
self.widget._state.selected = _FieldSelected.CAL
|
||
|
|
|
||
|
|
evt = TTkKeyEvent(TTkK.Character, "x", "", TTkK.NoModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
assert result is True
|
||
|
|
mock_show_form.assert_called_once()
|
||
|
|
|
||
|
|
def test_special_key_resets_digit(self):
|
||
|
|
"""Test that special keys reset the digit counter."""
|
||
|
|
self.widget._state.selected = _FieldSelected.YEARS
|
||
|
|
self.widget._state.digit = 3
|
||
|
|
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Up, "", TTkK.NoModifier)
|
||
|
|
self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
assert self.widget._state.digit == 0
|
||
|
|
|
||
|
|
def test_unknown_special_key_returns_false(self):
|
||
|
|
"""Test unknown special keys return False."""
|
||
|
|
self.widget._state.selected = _FieldSelected.YEARS
|
||
|
|
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, 9999, "", TTkK.NoModifier) # Unknown key
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
assert result is False
|
||
|
|
|
||
|
|
def test_non_digit_character_returns_true(self):
|
||
|
|
"""Test non-digit character input returns True but doesn't change date."""
|
||
|
|
self.widget._state.selected = _FieldSelected.YEARS
|
||
|
|
initial_date = self.widget.date()
|
||
|
|
|
||
|
|
evt = TTkKeyEvent(TTkK.Character, "a", "", TTkK.NoModifier)
|
||
|
|
result = self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
assert result is True
|
||
|
|
assert self.widget.date() == initial_date
|
||
|
|
|
||
|
|
|
||
|
|
class TestTTkDateWheelEvents:
|
||
|
|
"""Test wheel event handling."""
|
||
|
|
|
||
|
|
def setup_method(self):
|
||
|
|
"""Set up test fixtures."""
|
||
|
|
self.widget = TTkDate(date=datetime.date(2023, 6, 15))
|
||
|
|
|
||
|
|
def test_wheel_up_years_field(self):
|
||
|
|
"""Test wheel up in years field increments year."""
|
||
|
|
initial_date = self.widget.date()
|
||
|
|
|
||
|
|
evt = TTkMouseEvent(0, 0, TTkK.Wheel, TTkK.WHEEL_Up, TTkK.NoModifier, 1, '')
|
||
|
|
result = self.widget.wheelEvent(evt)
|
||
|
|
|
||
|
|
assert result is True
|
||
|
|
new_date = self.widget.date()
|
||
|
|
days_in_year = 366 if calendar.isleap(initial_date.year) else 365
|
||
|
|
expected_ordinal = initial_date.toordinal() + days_in_year
|
||
|
|
assert new_date.toordinal() == expected_ordinal
|
||
|
|
|
||
|
|
def test_wheel_down_months_field(self):
|
||
|
|
"""Test wheel down in months field decrements month."""
|
||
|
|
initial_date = self.widget.date()
|
||
|
|
|
||
|
|
evt = TTkMouseEvent(5, 0, TTkK.Wheel, TTkK.WHEEL_Down, TTkK.NoModifier, 1, '')
|
||
|
|
result = self.widget.wheelEvent(evt)
|
||
|
|
|
||
|
|
assert result is True
|
||
|
|
new_date = self.widget.date()
|
||
|
|
expected_ordinal = initial_date.toordinal() - 30
|
||
|
|
assert new_date.toordinal() == expected_ordinal
|
||
|
|
|
||
|
|
def test_wheel_resets_digit(self):
|
||
|
|
"""Test wheel events reset digit counter."""
|
||
|
|
self.widget._state.digit = 3
|
||
|
|
|
||
|
|
evt = TTkMouseEvent(0, 0, TTkK.Wheel, TTkK.WHEEL_Up, TTkK.NoModifier, 1, '')
|
||
|
|
self.widget.wheelEvent(evt)
|
||
|
|
|
||
|
|
assert self.widget._state.digit == 0
|
||
|
|
|
||
|
|
def test_wheel_outside_fields_no_effect(self):
|
||
|
|
"""Test wheel events outside fields have no effect."""
|
||
|
|
initial_date = self.widget.date()
|
||
|
|
|
||
|
|
# Wheel on separator position
|
||
|
|
evt = TTkMouseEvent(4, 0, TTkK.Wheel, TTkK.WHEEL_Up, TTkK.NoModifier, 1, '')
|
||
|
|
result = self.widget.wheelEvent(evt)
|
||
|
|
|
||
|
|
assert result is True
|
||
|
|
assert self.widget.date() == initial_date
|
||
|
|
|
||
|
|
|
||
|
|
class TestTTkDateShowForm:
|
||
|
|
"""Test calendar form functionality."""
|
||
|
|
|
||
|
|
def setup_method(self):
|
||
|
|
"""Set up test fixtures."""
|
||
|
|
self.widget = TTkDate(date=datetime.date(2023, 6, 15))
|
||
|
|
|
||
|
|
@patch('TermTk.TTkCore.helper.TTkHelper.overlay')
|
||
|
|
@patch('TermTk.TTkWidgets.datetime_date.TTkResizableFrame')
|
||
|
|
@patch('TermTk.TTkWidgets.datetime_date.TTkDateForm')
|
||
|
|
def test_show_form_date_selection_callback(self, mock_date_form, mock_frame, mock_overlay):
|
||
|
|
"""Test _showForm() date selection callback functionality."""
|
||
|
|
# Setup mocks
|
||
|
|
mock_frame_instance = MagicMock()
|
||
|
|
mock_form_instance = MagicMock()
|
||
|
|
mock_frame.return_value = mock_frame_instance
|
||
|
|
mock_date_form.return_value = mock_form_instance
|
||
|
|
|
||
|
|
# Mock the signal connection
|
||
|
|
mock_date_changed_signal = MagicMock()
|
||
|
|
mock_form_instance.dateChanged = mock_date_changed_signal
|
||
|
|
|
||
|
|
self.widget._showForm()
|
||
|
|
|
||
|
|
# Verify signal connection was attempted
|
||
|
|
mock_date_changed_signal.connect.assert_called_once()
|
||
|
|
|
||
|
|
# Get the connected slot function
|
||
|
|
connected_slot = mock_date_changed_signal.connect.call_args[0][0]
|
||
|
|
|
||
|
|
# Test the slot behavior
|
||
|
|
test_date = datetime.date(2024, 1, 1)
|
||
|
|
with patch.object(self.widget, 'setDate') as mock_set_date, \
|
||
|
|
patch.object(self.widget, 'setFocus') as mock_set_focus:
|
||
|
|
|
||
|
|
connected_slot(test_date)
|
||
|
|
|
||
|
|
# Verify the slot clears the signal, closes frame, sets date, and focuses widget
|
||
|
|
mock_date_changed_signal.clear.assert_called_once()
|
||
|
|
mock_frame_instance.close.assert_called_once()
|
||
|
|
mock_set_date.assert_called_once_with(test_date)
|
||
|
|
mock_set_focus.assert_called_once()
|
||
|
|
|
||
|
|
|
||
|
|
class TestTTkDateIntegration:
|
||
|
|
"""Integration tests for TTkDate widget."""
|
||
|
|
|
||
|
|
def setup_method(self):
|
||
|
|
"""Set up test fixtures."""
|
||
|
|
self.widget = TTkDate(date=datetime.date(2023, 6, 15))
|
||
|
|
|
||
|
|
def test_complete_date_entry_workflow(self):
|
||
|
|
"""Test complete date entry workflow."""
|
||
|
|
# Start with field selection
|
||
|
|
self.widget._state.selected = _FieldSelected.YEARS
|
||
|
|
|
||
|
|
# Enter year 2024
|
||
|
|
for digit in "2024":
|
||
|
|
evt = TTkKeyEvent(TTkK.Character, digit, "", TTkK.NoModifier)
|
||
|
|
self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
# Navigate to months
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Tab, "", TTkK.NoModifier)
|
||
|
|
self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
# Enter month 12
|
||
|
|
for digit in "12":
|
||
|
|
evt = TTkKeyEvent(TTkK.Character, digit, "", TTkK.NoModifier)
|
||
|
|
self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
# Navigate to days
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Tab, "", TTkK.NoModifier)
|
||
|
|
self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
# Enter day 25
|
||
|
|
for digit in "25":
|
||
|
|
evt = TTkKeyEvent(TTkK.Character, digit, "", TTkK.NoModifier)
|
||
|
|
self.widget.keyEvent(evt)
|
||
|
|
|
||
|
|
# Verify final date
|
||
|
|
assert self.widget.date() == datetime.date(2024, 12, 25)
|
||
|
|
|
||
|
|
def test_boundary_conditions_leap_year(self):
|
||
|
|
"""Test boundary conditions with leap year."""
|
||
|
|
# Set to leap year
|
||
|
|
leap_date = datetime.date(2024, 2, 29)
|
||
|
|
self.widget.setDate(leap_date)
|
||
|
|
|
||
|
|
# Verify leap year date is accepted
|
||
|
|
assert self.widget.date() == leap_date
|
||
|
|
|
||
|
|
# Test incrementing/decrementing around leap day
|
||
|
|
self.widget._state.selected = _FieldSelected.DAYS
|
||
|
|
|
||
|
|
# Decrement day
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Down, "", TTkK.NoModifier)
|
||
|
|
self.widget.keyEvent(evt)
|
||
|
|
assert self.widget.date() == datetime.date(2024, 2, 28)
|
||
|
|
|
||
|
|
# Increment back
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Up, "", TTkK.NoModifier)
|
||
|
|
self.widget.keyEvent(evt)
|
||
|
|
assert self.widget.date() == datetime.date(2024, 2, 29)
|
||
|
|
|
||
|
|
@patch.object(TTkDate, 'update')
|
||
|
|
def test_state_changes_trigger_updates(self, mock_update):
|
||
|
|
"""Test that state changes trigger UI updates."""
|
||
|
|
# Navigation should trigger update
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Tab, "", TTkK.NoModifier)
|
||
|
|
self.widget.keyEvent(evt)
|
||
|
|
mock_update.assert_called()
|
||
|
|
|
||
|
|
mock_update.reset_mock()
|
||
|
|
|
||
|
|
# Date changes should trigger update
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Up, "", TTkK.NoModifier)
|
||
|
|
self.widget.keyEvent(evt)
|
||
|
|
mock_update.assert_called()
|
||
|
|
|
||
|
|
def test_signal_emission_on_date_changes(self):
|
||
|
|
"""Test signal emission on various date changes."""
|
||
|
|
signals_received = []
|
||
|
|
|
||
|
|
def slot(date_value):
|
||
|
|
signals_received.append(date_value)
|
||
|
|
|
||
|
|
self.widget.dateChanged.connect(slot)
|
||
|
|
|
||
|
|
# Test programmatic date change
|
||
|
|
new_date = datetime.date(2025, 1, 1)
|
||
|
|
self.widget.setDate(new_date)
|
||
|
|
assert len(signals_received) == 1
|
||
|
|
assert signals_received[0] == new_date
|
||
|
|
|
||
|
|
# Test keyboard-induced date change
|
||
|
|
self.widget._state.selected = _FieldSelected.DAYS
|
||
|
|
evt = TTkKeyEvent(TTkK.SpecialKey, TTkK.Key_Up, "", TTkK.NoModifier)
|
||
|
|
self.widget.keyEvent(evt)
|
||
|
|
assert len(signals_received) == 2
|
||
|
|
assert signals_received[1] == datetime.date(2025, 1, 2)
|