Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions tests/e2e/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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!"
):
Expand Down
19 changes: 15 additions & 4 deletions tests/e2e/test_analytics_recurring_swapreview.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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')")
Expand Down
22 changes: 16 additions & 6 deletions tests/e2e/test_availability_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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')")
Expand All @@ -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")
Expand All @@ -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')")
Expand Down
18 changes: 14 additions & 4 deletions tests/e2e/test_bulk_notify.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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")
Expand All @@ -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')")
Expand Down
18 changes: 14 additions & 4 deletions tests/e2e/test_calendar_subscription.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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")
Expand All @@ -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')")

Expand Down
19 changes: 15 additions & 4 deletions tests/e2e/test_notif_assign_unpublish.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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')")
Expand Down
18 changes: 14 additions & 4 deletions tests/e2e/test_onboarding_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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")
Expand All @@ -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')")
Expand Down
18 changes: 14 additions & 4 deletions tests/e2e/test_smoke_full_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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")
Expand All @@ -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')")
Expand Down
38 changes: 38 additions & 0 deletions tests/unit/test_e2e_helpers.py
Original file line number Diff line number Diff line change
@@ -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"
Loading