diff --git a/tests/e2e/test_availability_solver.py b/tests/e2e/test_availability_solver.py new file mode 100644 index 00000000..fa3aef11 --- /dev/null +++ b/tests/e2e/test_availability_solver.py @@ -0,0 +1,71 @@ +"""Overnight B6 e2e — the solver must not assign a volunteer on time-off. + +Regression guard for an existing correctness property that had unit +coverage but no end-to-end enforcement: a volunteer whose time-off +covers the only event date must be left unassigned (the role stays +open) rather than double-booked. +""" + +from __future__ import annotations + +import pytest + +from tests.e2e._helpers import accept_invitation, invite_token, no_js_errors, rid, signup_admin + +pytestmark = pytest.mark.e2e + + +def test_timeoff_blocks_solver_assignment(live_server, new_context, page, db_path): + base = live_server + vol_email = f"vol+{rid()}@hope.e2e" + + signup_admin(page, base) + + page.goto(f"{base}/a/people") + page.click("button:has-text('Invite person')") + page.fill("#inv_name", "Dana Vol") + page.fill("#inv_email", vol_email) + page.select_option("#inv_role", "volunteer") + page.click("button:has-text('Send invite')") + page.wait_for_selector("#invite-result:has-text('Invitation sent')") + vol_page = accept_invitation(new_context(), base, invite_token(db_path, vol_email)) + + # Volunteer books time-off covering the event 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("#reason", "Vacation") + vol_page.click("button:has-text('Add time-off')") + vol_page.wait_for_selector("#timeoff-list:has-text('Vacation')") + + # Admin creates the only event on that exact date, one open role. + 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_start", "10:00") + page.fill("#ev_end", "11:30") + page.fill("input[name=role_name]", "volunteer") + page.fill("input[name=role_count]", "1") + page.click("button:has-text('Create event')") + page.wait_for_selector("#events-list:has-text('Sunday 10am Service')") + + # Solve, then publish. + page.goto(f"{base}/a/solver") + page.fill("#from_date", "2026-05-19") + page.fill("#to_date", "2026-06-30") + page.click("button:has-text('Run solver')") + page.wait_for_selector("#solver-result:has-text('Review solution')") + page.click("a:has-text('Review solution')") + page.wait_for_url("**/a/solution/**") + page.wait_for_selector("#publish-state") + page.click("button:has-text('Publish this solution')") + page.wait_for_selector("#publish-state:has-text('Unpublish')") + + # The on-time-off volunteer was NOT scheduled. + vol_page.goto(f"{base}/v/schedule") + vol_page.wait_for_selector("text=No assignments yet") + + no_js_errors(vol_page)