"""End-to-end Playwright tests for the DateRangePicker component. Exercises the behaviour layers the rendering tests cannot reach (``date_range_picker.js``): segmented digit entry with right-to-left placeholder fill and auto-advance, Backspace reverting a part, the calendar popup's anchor-style range picking, presets, the Cancel / Clear / Select footer, and the ``filter_bar.js`` serialization of the hidden ISO inputs into a ``DateCriterion``. Like the other filter-bar e2e modules, the bar is served from its own minimal URLconf (no auth, no CSS) — the JS only cares about the DOM. """ import datetime import json import urllib.parse import pytest from django.http import HttpResponse from django.test import override_settings from common.components import PurchaseFilterBar from django.urls import path def _bar_page(filter_json: str = "") -> str: return f""" Date range picker E2E {PurchaseFilterBar(filter_json=filter_json, preset_list_url="/p/l", preset_save_url="/p/s")} """ def empty_bar_view(request): return HttpResponse(_bar_page()) def prefilled_bar_view(request): filter_json = json.dumps( { "date_purchased": { "value": "2024-03-15", "value2": "2024-09-20", "modifier": "BETWEEN", } } ) return HttpResponse(_bar_page(filter_json)) urlpatterns = [ path("test-date-range-picker/", empty_bar_view), path("test-date-range-picker-prefilled/", prefilled_bar_view), ] PICKER = '[data-date-range-picker][data-input-name-prefix="filter-date-purchased"]' POPUP = PICKER + " [data-date-range-calendar]" HIDDEN_MIN = 'input[name="filter-date-purchased-min"]' HIDDEN_MAX = 'input[name="filter-date-purchased-max"]' def _segment(page, side: str, part: str): return page.locator( f'{PICKER} input[data-date-side="{side}"][data-date-part="{part}"]' ) def _day_cell(page, iso_date: str): return page.locator( f'{PICKER} [data-date-range-grid] button[data-date="{iso_date}"]' ) def _popup_is_open(page) -> bool: return "hidden" not in (page.locator(POPUP).get_attribute("class") or "") def _submit_filter_bar(page): with page.expect_navigation(): page.evaluate( "document.getElementById('filter-bar-form')" ".dispatchEvent(new Event('submit', {cancelable: true}))" ) def _filter_from_url(url: str) -> dict: query = urllib.parse.urlparse(url).query params = urllib.parse.parse_qs(query) raw = params.get("filter", [""])[0] return json.loads(raw) if raw else {} # ── Segmented manual entry ────────────────────────────────────────────────── @pytest.mark.django_db @override_settings(ROOT_URLCONF="e2e.test_date_range_picker_e2e") def test_typing_fills_parts_and_serializes_between(live_server, page): """Digits flow through the parts (DD → MM → YYYY → DD …) with auto-advance, ending in a BETWEEN criterion on submit.""" page.goto(live_server.url + "/test-date-range-picker/") _segment(page, "min", "day").click() page.keyboard.type("1503202420092024") assert page.locator(HIDDEN_MIN).input_value() == "2024-03-15" assert page.locator(HIDDEN_MAX).input_value() == "2024-09-20" _submit_filter_bar(page) parsed = _filter_from_url(page.url) assert parsed == { "date_purchased": { "value": "2024-03-15", "value2": "2024-09-20", "modifier": "BETWEEN", } } @pytest.mark.django_db @override_settings(ROOT_URLCONF="e2e.test_date_range_picker_e2e") def test_placeholder_fills_from_the_right(live_server, page): """Typing 19 into the YYYY part shows YYY1 then YY19.""" page.goto(live_server.url + "/test-date-range-picker/") year_segment = _segment(page, "min", "year") year_segment.click() page.keyboard.press("1") assert year_segment.input_value() == "YYY1" page.keyboard.press("9") assert year_segment.input_value() == "YY19" @pytest.mark.django_db @override_settings(ROOT_URLCONF="e2e.test_date_range_picker_e2e") def test_min_side_only_serializes_greater_than(live_server, page): page.goto(live_server.url + "/test-date-range-picker/") _segment(page, "min", "day").click() page.keyboard.type("15062024") _submit_filter_bar(page) parsed = _filter_from_url(page.url) assert parsed == { "date_purchased": {"value": "2024-06-15", "modifier": "GREATER_THAN"} } @pytest.mark.django_db @override_settings(ROOT_URLCONF="e2e.test_date_range_picker_e2e") def test_backspace_reverts_part_to_placeholder(live_server, page): page.goto(live_server.url + "/test-date-range-picker/") _segment(page, "min", "day").click() page.keyboard.type("15032024") assert page.locator(HIDDEN_MIN).input_value() == "2024-03-15" month_segment = _segment(page, "min", "month") month_segment.click() page.keyboard.press("Backspace") assert month_segment.input_value() == "" # An incomplete date no longer commits to the hidden input. assert page.locator(HIDDEN_MIN).input_value() == "" @pytest.mark.django_db @override_settings(ROOT_URLCONF="e2e.test_date_range_picker_e2e") def test_only_numbers_can_be_typed(live_server, page): page.goto(live_server.url + "/test-date-range-picker/") day_segment = _segment(page, "min", "day") day_segment.click() page.keyboard.type("ab-/") assert day_segment.input_value() == "" @pytest.mark.django_db @override_settings(ROOT_URLCONF="e2e.test_date_range_picker_e2e") def test_invalid_calendar_date_does_not_commit(live_server, page): """31-02-2024 fills all parts but is not a real date — no hidden value.""" page.goto(live_server.url + "/test-date-range-picker/") _segment(page, "min", "day").click() page.keyboard.type("31022024") assert page.locator(HIDDEN_MIN).input_value() == "" @pytest.mark.django_db @override_settings(ROOT_URLCONF="e2e.test_date_range_picker_e2e") def test_clicking_container_activates_first_part(live_server, page): page.goto(live_server.url + "/test-date-range-picker/") page.locator(PICKER + " [data-date-range-field]").click(position={"x": 5, "y": 5}) focused = page.evaluate( "document.activeElement.getAttribute('data-date-part') + ':' +" "document.activeElement.getAttribute('data-date-side')" ) assert focused == "day:min" # ── Calendar popup ────────────────────────────────────────────────────────── def _open_calendar(page): page.locator(PICKER + " [data-date-range-calendar-toggle]").click() def _current_month_iso(day_of_month: int) -> str: today = datetime.date.today() return today.replace(day=day_of_month).isoformat() @pytest.mark.django_db @override_settings(ROOT_URLCONF="e2e.test_date_range_picker_e2e") def test_calendar_pick_range_then_select(live_server, page): page.goto(live_server.url + "/test-date-range-picker/") _open_calendar(page) assert _popup_is_open(page) first_pick = _current_month_iso(10) second_pick = _current_month_iso(20) _day_cell(page, first_pick).click() assert page.locator(HIDDEN_MIN).input_value() == first_pick assert page.locator(HIDDEN_MAX).input_value() == "" _day_cell(page, second_pick).click() assert page.locator(HIDDEN_MAX).input_value() == second_pick page.locator(PICKER + " [data-date-range-select]").click() assert not _popup_is_open(page) _submit_filter_bar(page) parsed = _filter_from_url(page.url) assert parsed == { "date_purchased": { "value": first_pick, "value2": second_pick, "modifier": "BETWEEN", } } @pytest.mark.django_db @override_settings(ROOT_URLCONF="e2e.test_date_range_picker_e2e") def test_picking_before_start_restarts_the_range(live_server, page): """With the StartDate anchored, picking an earlier date clears the range and the clicked date becomes the new StartDate.""" page.goto(live_server.url + "/test-date-range-picker/") _open_calendar(page) _day_cell(page, _current_month_iso(20)).click() _day_cell(page, _current_month_iso(10)).click() assert page.locator(HIDDEN_MIN).input_value() == _current_month_iso(10) assert page.locator(HIDDEN_MAX).input_value() == "" @pytest.mark.django_db @override_settings(ROOT_URLCONF="e2e.test_date_range_picker_e2e") def test_completed_range_anchor_moves_to_end(live_server, page): """After both dates are picked the EndDate becomes the anchor, so a further pick inside the range moves the StartDate.""" page.goto(live_server.url + "/test-date-range-picker/") _open_calendar(page) _day_cell(page, _current_month_iso(10)).click() _day_cell(page, _current_month_iso(20)).click() _day_cell(page, _current_month_iso(15)).click() assert page.locator(HIDDEN_MIN).input_value() == _current_month_iso(15) assert page.locator(HIDDEN_MAX).input_value() == _current_month_iso(20) @pytest.mark.django_db @override_settings(ROOT_URLCONF="e2e.test_date_range_picker_e2e") def test_preset_fills_both_dates(live_server, page): page.goto(live_server.url + "/test-date-range-picker/") _open_calendar(page) page.locator(PICKER + ' [data-date-range-preset="last_7_days"]').click() today = datetime.date.today() assert ( page.locator(HIDDEN_MIN).input_value() == (today - datetime.timedelta(days=6)).isoformat() ) assert page.locator(HIDDEN_MAX).input_value() == today.isoformat() # Presets keep the popup open; Select commits and closes. assert _popup_is_open(page) @pytest.mark.django_db @override_settings(ROOT_URLCONF="e2e.test_date_range_picker_e2e") def test_clear_clears_dates_but_keeps_popup_open(live_server, page): page.goto(live_server.url + "/test-date-range-picker/") _open_calendar(page) _day_cell(page, _current_month_iso(10)).click() _day_cell(page, _current_month_iso(20)).click() page.locator(PICKER + " [data-date-range-clear]").click() assert page.locator(HIDDEN_MIN).input_value() == "" assert page.locator(HIDDEN_MAX).input_value() == "" assert _popup_is_open(page) @pytest.mark.django_db @override_settings(ROOT_URLCONF="e2e.test_date_range_picker_e2e") def test_cancel_clears_dates_and_closes_popup(live_server, page): page.goto(live_server.url + "/test-date-range-picker/") _open_calendar(page) _day_cell(page, _current_month_iso(10)).click() _day_cell(page, _current_month_iso(20)).click() page.locator(PICKER + " [data-date-range-cancel]").click() assert page.locator(HIDDEN_MIN).input_value() == "" assert page.locator(HIDDEN_MAX).input_value() == "" assert not _popup_is_open(page) # ── Prefill round-trip ────────────────────────────────────────────────────── @pytest.mark.django_db @override_settings(ROOT_URLCONF="e2e.test_date_range_picker_e2e") def test_prefilled_picker_round_trips_unchanged(live_server, page): page.goto(live_server.url + "/test-date-range-picker-prefilled/") assert _segment(page, "min", "day").input_value() == "15" assert _segment(page, "min", "month").input_value() == "03" assert _segment(page, "min", "year").input_value() == "2024" assert _segment(page, "max", "day").input_value() == "20" assert page.locator(HIDDEN_MIN).input_value() == "2024-03-15" assert page.locator(HIDDEN_MAX).input_value() == "2024-09-20" _submit_filter_bar(page) parsed = _filter_from_url(page.url) assert parsed["date_purchased"] == { "value": "2024-03-15", "value2": "2024-09-20", "modifier": "BETWEEN", }