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
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) |