diff --git a/tests/e2e/_helpers.py b/tests/e2e/_helpers.py index de376ca3..dda9193a 100644 --- a/tests/e2e/_helpers.py +++ b/tests/e2e/_helpers.py @@ -30,6 +30,23 @@ def next_sunday_iso() -> str: return (today + timedelta(days=days_until_sunday)).isoformat() +def solver_window_around(seed_iso: str) -> tuple[str, str]: + """Return (from_date, to_date) ISO strings that safely bracket a seed + event date for `/a/solver`. + + Flows that seed an event with `next_sunday_iso()` then run the solver + over a window must pick from/to dates that comfortably contain the + seed. Fixed literal windows (e.g. from=2026-05-19, to=2026-06-30) + rot as time passes and drop the seed event outside the window. This + helper returns a 3-week-before / 3-week-after bracket around the + seed, mirroring the pattern the sweep replaces. + """ + seed = date.fromisoformat(seed_iso) + from_date = (seed - timedelta(days=21)).isoformat() + to_date = (seed + timedelta(days=21)).isoformat() + return from_date, to_date + + def signup_admin( page, base_url, *, org="Hope Chapel", name="Admin Dana", email=None, password="HopePass123!" ): diff --git a/tests/e2e/test_analytics_recurring_swapreview.py b/tests/e2e/test_analytics_recurring_swapreview.py index 10561164..01978043 100644 --- a/tests/e2e/test_analytics_recurring_swapreview.py +++ b/tests/e2e/test_analytics_recurring_swapreview.py @@ -7,17 +7,28 @@ import pytest -from tests.e2e._helpers import accept_invitation, invite_token, no_js_errors, rid, signup_admin +from tests.e2e._helpers import ( + accept_invitation, + invite_token, + next_sunday_iso, + no_js_errors, + rid, + signup_admin, + solver_window_around, +) pytestmark = pytest.mark.e2e +EV_DATE = next_sunday_iso() +FROM_DATE, TO_DATE = solver_window_around(EV_DATE) + def _new_event(page, base, etype): page.goto(f"{base}/a/events") page.click("button:has-text('New event')") page.wait_for_selector("#ev_type", state="visible") page.fill("#ev_type", etype) - page.fill("#ev_date", "2026-06-07") + page.fill("#ev_date", EV_DATE) page.fill("#ev_start", "10:00") page.fill("#ev_end", "11:30") page.fill("input[name=role_name]", "volunteer") @@ -28,8 +39,8 @@ def _new_event(page, base, etype): def _solve_and_publish(page, base): page.goto(f"{base}/a/solver") - page.fill("#from_date", "2026-05-19") - page.fill("#to_date", "2026-06-30") + page.fill("#from_date", FROM_DATE) + page.fill("#to_date", TO_DATE) page.click("button:has-text('Run solver')") page.wait_for_selector("#solver-result:has-text('Review solution')") page.click("a:has-text('Review solution')") diff --git a/tests/e2e/test_availability_solver.py b/tests/e2e/test_availability_solver.py index fa3aef11..24a0037d 100644 --- a/tests/e2e/test_availability_solver.py +++ b/tests/e2e/test_availability_solver.py @@ -10,7 +10,15 @@ import pytest -from tests.e2e._helpers import accept_invitation, invite_token, no_js_errors, rid, signup_admin +from tests.e2e._helpers import ( + accept_invitation, + invite_token, + next_sunday_iso, + no_js_errors, + rid, + signup_admin, + solver_window_around, +) pytestmark = pytest.mark.e2e @@ -31,10 +39,12 @@ def test_timeoff_blocks_solver_assignment(live_server, new_context, page, db_pat vol_page = accept_invitation(new_context(), base, invite_token(db_path, vol_email)) # Volunteer books time-off covering the event date. + ev_date = next_sunday_iso() + from_date, to_date = solver_window_around(ev_date) vol_page.goto(f"{base}/v/availability") vol_page.wait_for_selector("#start_date") - vol_page.fill("#start_date", "2026-06-07") - vol_page.fill("#end_date", "2026-06-07") + vol_page.fill("#start_date", ev_date) + vol_page.fill("#end_date", ev_date) vol_page.fill("#reason", "Vacation") vol_page.click("button:has-text('Add time-off')") vol_page.wait_for_selector("#timeoff-list:has-text('Vacation')") @@ -44,7 +54,7 @@ def test_timeoff_blocks_solver_assignment(live_server, new_context, page, db_pat page.click("button:has-text('New event')") page.wait_for_selector("#ev_type", state="visible") page.fill("#ev_type", "Sunday 10am Service") - page.fill("#ev_date", "2026-06-07") + page.fill("#ev_date", ev_date) page.fill("#ev_start", "10:00") page.fill("#ev_end", "11:30") page.fill("input[name=role_name]", "volunteer") @@ -54,8 +64,8 @@ def test_timeoff_blocks_solver_assignment(live_server, new_context, page, db_pat # Solve, then publish. page.goto(f"{base}/a/solver") - page.fill("#from_date", "2026-05-19") - page.fill("#to_date", "2026-06-30") + page.fill("#from_date", from_date) + page.fill("#to_date", to_date) page.click("button:has-text('Run solver')") page.wait_for_selector("#solver-result:has-text('Review solution')") page.click("a:has-text('Review solution')") diff --git a/tests/e2e/test_bulk_notify.py b/tests/e2e/test_bulk_notify.py index 2ad90bb2..2210b6ac 100644 --- a/tests/e2e/test_bulk_notify.py +++ b/tests/e2e/test_bulk_notify.py @@ -8,7 +8,15 @@ import pytest -from tests.e2e._helpers import accept_invitation, invite_token, no_js_errors, rid, signup_admin +from tests.e2e._helpers import ( + accept_invitation, + invite_token, + next_sunday_iso, + no_js_errors, + rid, + signup_admin, + solver_window_around, +) pytestmark = pytest.mark.e2e @@ -28,11 +36,13 @@ def test_notify_published_schedule_reaches_inbox(live_server, new_context, page, page.wait_for_selector("#invite-result:has-text('Invitation sent')") vol_page = accept_invitation(new_context(), base, invite_token(db_path, vol_email)) + ev_date = next_sunday_iso() + from_date, to_date = solver_window_around(ev_date) page.goto(f"{base}/a/events") page.click("button:has-text('New event')") page.wait_for_selector("#ev_type", state="visible") page.fill("#ev_type", "Sunday 10am Service") - page.fill("#ev_date", "2026-06-07") + page.fill("#ev_date", ev_date) page.fill("#ev_start", "10:00") page.fill("#ev_end", "11:30") page.fill("input[name=role_name]", "volunteer") @@ -41,8 +51,8 @@ def test_notify_published_schedule_reaches_inbox(live_server, new_context, page, page.wait_for_selector("#events-list:has-text('Sunday 10am Service')") page.goto(f"{base}/a/solver") - page.fill("#from_date", "2026-05-19") - page.fill("#to_date", "2026-06-30") + page.fill("#from_date", from_date) + page.fill("#to_date", to_date) page.click("button:has-text('Run solver')") page.wait_for_selector("#solver-result:has-text('Review solution')") page.click("a:has-text('Review solution')") diff --git a/tests/e2e/test_calendar_subscription.py b/tests/e2e/test_calendar_subscription.py index bb9d8eb2..674fd275 100644 --- a/tests/e2e/test_calendar_subscription.py +++ b/tests/e2e/test_calendar_subscription.py @@ -10,7 +10,15 @@ import pytest -from tests.e2e._helpers import accept_invitation, invite_token, no_js_errors, rid, signup_admin +from tests.e2e._helpers import ( + accept_invitation, + invite_token, + next_sunday_iso, + no_js_errors, + rid, + signup_admin, + solver_window_around, +) pytestmark = pytest.mark.e2e @@ -38,11 +46,13 @@ def test_subscription_gates_on_publish(live_server, new_context, page, db_path): page.wait_for_selector("#invite-result:has-text('Invitation sent')") vol_page = accept_invitation(new_context(), base, invite_token(db_path, vol_email)) + ev_date = next_sunday_iso() + from_date, to_date = solver_window_around(ev_date) page.goto(f"{base}/a/events") page.click("button:has-text('New event')") page.wait_for_selector("#ev_type", state="visible") page.fill("#ev_type", "Sunday 10am Service") - page.fill("#ev_date", "2026-06-07") + page.fill("#ev_date", ev_date) page.fill("#ev_start", "10:00") page.fill("#ev_end", "11:30") page.fill("input[name=role_name]", "volunteer") @@ -52,8 +62,8 @@ def test_subscription_gates_on_publish(live_server, new_context, page, db_path): # Solve — produces a DRAFT (unpublished) solution + assignment. page.goto(f"{base}/a/solver") - page.fill("#from_date", "2026-05-19") - page.fill("#to_date", "2026-06-30") + page.fill("#from_date", from_date) + page.fill("#to_date", to_date) page.click("button:has-text('Run solver')") page.wait_for_selector("#solver-result:has-text('Review solution')") diff --git a/tests/e2e/test_notif_assign_unpublish.py b/tests/e2e/test_notif_assign_unpublish.py index 75ad86b1..658ea9d9 100644 --- a/tests/e2e/test_notif_assign_unpublish.py +++ b/tests/e2e/test_notif_assign_unpublish.py @@ -9,17 +9,28 @@ import pytest -from tests.e2e._helpers import accept_invitation, invite_token, no_js_errors, rid, signup_admin +from tests.e2e._helpers import ( + accept_invitation, + invite_token, + next_sunday_iso, + no_js_errors, + rid, + signup_admin, + solver_window_around, +) pytestmark = pytest.mark.e2e +EV_DATE = next_sunday_iso() +FROM_DATE, TO_DATE = solver_window_around(EV_DATE) + def _new_event(page, base, etype="Sunday 10am Service", role="volunteer", count="1"): page.goto(f"{base}/a/events") page.click("button:has-text('New event')") page.wait_for_selector("#ev_type", state="visible") page.fill("#ev_type", etype) - page.fill("#ev_date", "2026-06-07") + page.fill("#ev_date", EV_DATE) page.fill("#ev_start", "10:00") page.fill("#ev_end", "11:30") page.fill("input[name=role_name]", role) @@ -30,8 +41,8 @@ def _new_event(page, base, etype="Sunday 10am Service", role="volunteer", count= def _solve_and_publish(page, base): page.goto(f"{base}/a/solver") - page.fill("#from_date", "2026-05-19") - page.fill("#to_date", "2026-06-30") + page.fill("#from_date", FROM_DATE) + page.fill("#to_date", TO_DATE) page.click("button:has-text('Run solver')") page.wait_for_selector("#solver-result:has-text('Review solution')") page.click("a:has-text('Review solution')") diff --git a/tests/e2e/test_onboarding_wizard.py b/tests/e2e/test_onboarding_wizard.py index b184f96b..eeb901ea 100644 --- a/tests/e2e/test_onboarding_wizard.py +++ b/tests/e2e/test_onboarding_wizard.py @@ -9,7 +9,15 @@ import pytest -from tests.e2e._helpers import accept_invitation, invite_token, no_js_errors, rid, signup_admin +from tests.e2e._helpers import ( + accept_invitation, + invite_token, + next_sunday_iso, + no_js_errors, + rid, + signup_admin, + solver_window_around, +) pytestmark = pytest.mark.e2e @@ -40,11 +48,13 @@ def test_fresh_admin_completes_wizard(live_server, new_context, page, db_path): _progress(page, base, "1 of 4 done") # 2) Create an event. + ev_date = next_sunday_iso() + from_date, to_date = solver_window_around(ev_date) page.goto(f"{base}/a/events") page.click("button:has-text('New event')") page.wait_for_selector("#ev_type", state="visible") page.fill("#ev_type", "Sunday 10am Service") - page.fill("#ev_date", "2026-06-07") + page.fill("#ev_date", ev_date) page.fill("#ev_start", "10:00") page.fill("#ev_end", "11:30") page.fill("input[name=role_name]", "volunteer") @@ -57,8 +67,8 @@ def test_fresh_admin_completes_wizard(live_server, new_context, page, db_path): # sequence (the solver page only holds the result until you navigate # away, so the review link must be clicked without leaving). page.goto(f"{base}/a/solver") - page.fill("#from_date", "2026-05-19") - page.fill("#to_date", "2026-06-30") + page.fill("#from_date", from_date) + page.fill("#to_date", to_date) page.click("button:has-text('Run solver')") page.wait_for_selector("#solver-result:has-text('Review solution')") page.click("a:has-text('Review solution')") diff --git a/tests/e2e/test_smoke_full_loop.py b/tests/e2e/test_smoke_full_loop.py index 6e5ab15b..9bc00ed5 100644 --- a/tests/e2e/test_smoke_full_loop.py +++ b/tests/e2e/test_smoke_full_loop.py @@ -9,7 +9,15 @@ import pytest -from tests.e2e._helpers import accept_invitation, invite_token, no_js_errors, rid, signup_admin +from tests.e2e._helpers import ( + accept_invitation, + invite_token, + next_sunday_iso, + no_js_errors, + rid, + signup_admin, + solver_window_around, +) pytestmark = pytest.mark.e2e @@ -37,11 +45,13 @@ def test_full_loop(live_server, new_context, page, db_path): vol_page = accept_invitation(new_context(), base, tok) # Admin creates an event with a role the solver can fill. + ev_date = next_sunday_iso() + from_date, to_date = solver_window_around(ev_date) page.goto(f"{base}/a/events") page.click("button:has-text('New event')") page.wait_for_selector("#ev_type", state="visible") page.fill("#ev_type", "Sunday 10am Service") - page.fill("#ev_date", "2026-06-07") + page.fill("#ev_date", ev_date) page.fill("#ev_start", "10:00") page.fill("#ev_end", "11:30") page.fill("input[name=role_name]", "volunteer") @@ -51,8 +61,8 @@ def test_full_loop(live_server, new_context, page, db_path): # Solve. page.goto(f"{base}/a/solver") - page.fill("#from_date", "2026-05-19") - page.fill("#to_date", "2026-06-30") + page.fill("#from_date", from_date) + page.fill("#to_date", to_date) page.click("button:has-text('Run solver')") page.wait_for_selector("#solver-result:has-text('Review solution')") page.click("a:has-text('Review solution')") diff --git a/tests/unit/test_e2e_helpers.py b/tests/unit/test_e2e_helpers.py new file mode 100644 index 00000000..522b9577 --- /dev/null +++ b/tests/unit/test_e2e_helpers.py @@ -0,0 +1,38 @@ +"""Invariants for `tests/e2e/_helpers` date utilities. + +These lock in the properties the e2e-lane relies on: +- `next_sunday_iso()` returns a Sunday at least 7 days in the future. +- `solver_window_around(seed)` returns (from, to) that comfortably + brackets `seed` so the seed always falls inside the solver window. +""" + +from __future__ import annotations + +from datetime import date + +from tests.e2e._helpers import next_sunday_iso, solver_window_around + + +def test_next_sunday_iso_is_a_future_sunday(): + seed = date.fromisoformat(next_sunday_iso()) + assert seed.weekday() == 6, "helper must return a Sunday" + assert (seed - date.today()).days >= 7, "helper must return a date at least 7 days from today" + + +def test_solver_window_brackets_seed(): + seed_iso = next_sunday_iso() + seed = date.fromisoformat(seed_iso) + from_iso, to_iso = solver_window_around(seed_iso) + from_d = date.fromisoformat(from_iso) + to_d = date.fromisoformat(to_iso) + assert from_d < seed < to_d, "seed must fall strictly inside (from, to)" + assert (seed - from_d).days >= 7, "from_date must be well before seed" + assert (to_d - seed).days >= 7, "to_date must be well after seed" + + +def test_solver_window_deterministic_from_seed(): + from_a, to_a = solver_window_around("2026-08-02") + from_b, to_b = solver_window_around("2026-08-02") + assert (from_a, to_a) == (from_b, to_b) + assert from_a == "2026-07-12" + assert to_a == "2026-08-23"