mathly / tests /test_my_tasks.py
3v324v23's picture
Add tests for student task list and quiz modes functionality
0ba5932
Raw History Blame Contribute Delete
13.2 kB
"""The student's task list (GET /api/math/my-tasks): their assignments, focus practice given to them only, the final
quizzes open to them, and a worksheet returned for a fix; nobody else's."""
import asyncio
from datetime import datetime, timedelta, timezone
from app.main import create_app
from test_assignments import STUDENT, TEACHER, MemoryAssignments, Sittings, client
from test_assignments import FocusAuth, CLASSMATE
from test_quiz_modes import MemoryQuizModes
from test_worksheets import MemoryWorksheets, photos, upload
def at(minutes):
return (datetime.now(timezone.utc) + timedelta(minutes=minutes)).isoformat()
def make_app():
app = create_app(auth=FocusAuth())
app.state.assignments, app.state.answers = MemoryAssignments(), Sittings()
app.state.worksheets, app.state.quiz_modes = MemoryWorksheets(), MemoryQuizModes()
return app
def test_the_task_list_holds_the_students_assignments_finals_and_returned_work():
app = make_app()
app.state.quiz_modes.add_window({"class_id": 3, "unit_id": "t", "opens_at": at(-5), "closes_at": at(120)})
async def check():
async with client(app, TEACHER) as teacher, client(app, STUDENT) as student, client(app, CLASSMATE) as classmate:
wid = (await upload(teacher)).json()["id"]
await teacher.post("/api/classes/3/assignments", json={"title": "ש״ב", "due_on": "2026-11-12",
"items": [{"kind": "worksheet", "id": wid}]})
await teacher.post("/api/classes/3/assignments", json={"title": "חיזוק", "due_on": "2026-11-15",
"items": [{"kind": "skill", "id": "t_ext"}],
"student_emails": ["classmate@x.test"]})
sub = (await student.put(f"/api/math/worksheets/{wid}/submission", files=photos(1))).json()
await teacher.post(f"/api/classes/3/worksheets/{wid}/submissions/{sub['id']}/return", json={"comment": "לתקן"})
mine = (await student.get("/api/math/my-tasks")).json()
assert [a["title"] for a in mine["assignments"]["assignments"]] == ["ש״ב"] # not the classmate's focus practice
(returned,) = mine["assignments"]["submissions"]
assert (returned["state"], returned["return_comment"]) == ("returned", "לתקן")
(final,) = mine["finals"]
assert final["window"]["unit_id"] == "t" and final["start"] is None
theirs = (await classmate.get("/api/math/my-tasks?student_id=" + STUDENT)).json() # an id is not taken
assert [a["title"] for a in theirs["assignments"]["assignments"]] == ["חיזוק", "ש״ב"]
assert theirs["assignments"]["submissions"] == []
asyncio.run(check())
def test_parts_that_cannot_be_read_are_none_and_signing_in_is_required():
app = make_app()
app.state.assignments = MemoryAssignments(fail=True)
class NoFinals(MemoryQuizModes):
def windows(self, class_ids):
from app.quiz_modes import QuizModesUnavailable
raise QuizModesUnavailable("quiz_modes_unavailable")
app.state.quiz_modes = NoFinals()
async def check():
async with client(app, STUDENT) as student:
got = (await student.get("/api/math/my-tasks")).json()
assert got == {"assignments": None, "finals": None}
async with client(app, "nobody") as nobody:
assert (await nobody.get("/api/math/my-tasks")).status_code == 401
asyncio.run(check())
# ---- in a browser: the home summary and the list, nearest first, one state each
import pytest # noqa: E402
playwright = pytest.importorskip("playwright.sync_api")
@pytest.fixture
def task_server():
import socket
import threading
import time
import uvicorn
app = make_app()
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
server = uvicorn.Server(uvicorn.Config(app, log_level="error"))
thread = threading.Thread(target=server.run, kwargs={"sockets": [sock]}, daemon=True)
thread.start()
try:
while not server.started:
time.sleep(.01)
yield f"http://127.0.0.1:{sock.getsockname()[1]}", app
finally:
server.should_exit = True
thread.join(timeout=5)
def test_the_student_sees_one_list_of_owed_work_nearest_first(task_server):
import httpx
from test_focus_browser import session
url, app = task_server
app.state.quiz_modes.add_window({"class_id": 3, "unit_id": "t", "opens_at": at(-5), "closes_at": at(120)})
app.state.assignments.add({"class_id": 3, "title": "חזרה", "note": "", "due_on": "2099-11-20",
"items": [{"kind": "skill", "id": "t_ext"}], "student_emails": None})
with httpx.Client(base_url=url, headers={"Authorization": f"Bearer {TEACHER}"}) as teacher, \
httpx.Client(base_url=url, headers={"Authorization": f"Bearer {STUDENT}"}) as student:
wid = teacher.post("/api/classes/3/worksheets", data={"title": "דף"}, files=photos(1)).json()["id"]
teacher.post("/api/classes/3/assignments", json={"title": "ש״ב", "due_on": "2099-11-12", "items": [{"kind": "worksheet", "id": wid}]})
sub = student.put(f"/api/math/worksheets/{wid}/submission", files=photos(1)).json()
teacher.post(f"/api/classes/3/worksheets/{wid}/submissions/{sub['id']}/return", json={"comment": "לתקן את 2", "due_on": "2099-11-14"})
with playwright.sync_playwright() as p:
browser = p.chromium.launch()
try:
page, errors = session(browser, STUDENT)
page.goto(url)
page.locator('.mx-nav [data-view="home"]').click()
summary = page.locator(".mx-tasks-sum")
playwright.expect(summary).to_contain_text("3 משימות פתוחות")
playwright.expect(summary).to_contain_text("הקרובה: בוחן מסכם: טכניקה אלגברית 1")
summary.click()
rows = page.locator(".mx-tasks-sheet .mx-task")
playwright.expect(rows).to_have_count(3)
playwright.expect(rows.locator("b")).to_have_text(["בוחן מסכם: טכניקה אלגברית 1", "ש״ב", "חזרה"])
playwright.expect(rows.locator(".mx-task-chip")).to_have_text(["עוד לא התחיל", "הוחזר לתיקון", "עוד לא התחיל"])
playwright.expect(rows.nth(1)).to_contain_text("לתקן את 2")
rows.nth(1).locator("[data-ws-open]").click() # opens the returned worksheet to fix it
playwright.expect(page.locator(".mx-ws-sheet .mx-ws-returned")).to_be_visible()
# nothing owed: the list says so and offers the way on
page.route("**/api/math/my-tasks", lambda route: route.fulfill(json={
"assignments": {"assignments": [], "sittings": [], "worksheets": [], "submissions": [], "today": "2026-11-06"}, "finals": []}))
page.locator(".mx-ws-sheet .mx-x").click()
page.locator(".mx-tasks-sum").click()
playwright.expect(page.locator(".mx-tasks-sheet [role=status]")).to_contain_text("אין מה להגיש עכשיו")
playwright.expect(page.locator('.mx-tasks-sheet .mx-btn[data-act="grades"]')).to_be_visible()
assert not errors, errors
finally:
browser.close()
def test_the_grades_page_labels_each_grade_and_opens_its_result(task_server):
from test_focus_browser import session
url, app = task_server
q = lambda earned, skills=("t_ext",): {"points": 25, "earned": earned, "state": "checked" if earned is not None else "waiting", "skills": list(skills)}
final = {"id": "f1", "unit_id": "t", "mode": "final", "released": True, "grade": 62, "open": 0, "window_id": "w",
"submitted_at": "2026-11-05T09:00:00Z", "time_up": False, "attempts": ["a1", "a2", "a3", "a4"],
"questions": [q(25), q(12), q(25), q(0)]}
best = {**final, "id": "p1", "mode": "practice", "grade": 85, "submitted_at": "2026-11-02T09:00:00Z", "attempts": ["b1", "b2", "b3", "b4"]}
waiting = {**final, "id": "p2", "mode": "practice", "grade": None, "open": 1, "submitted_at": "2026-11-04T09:00:00Z",
"attempts": ["c1", "c2", "c3", "c4"], "questions": [q(25), q(None), q(25), q(25)]}
data = {"units": [{"unit_id": "t", "name": "טכניקה אלגברית 1", "official": final, "practice_best": best,
"practice_latest": waiting, "sittings": [final, waiting, best]}],
"quizzes_known": True, "comments": [{"attempt_id": "a2", "question": "בוחן מסכם · שאלה 2", "score": 50, "question_id": "quiz:t:1"}],
"worksheets": [{"id": "s1", "worksheet_id": "w1", "title": "דף פילוג", "assignment": "חזרה", "unit_id": "t",
"checked_at": "2026-11-05T09:00:00Z", "score": 90, "comment": "יפה", "state": "checked"}]}
with playwright.sync_playwright() as p:
browser = p.chromium.launch()
try:
page, errors = session(browser, STUDENT)
page.route("**/api/math/my-grades", lambda route: route.fulfill(json=data))
page.route("**/api/math/quiz-sittings?unit_id=t", lambda route: route.fulfill(json={"sittings": [final, waiting, best]}))
page.goto(url)
page.locator('.mx-nav [data-view="home"]').click()
page.locator('.mx-nav [data-view="grades"]').click()
g = page.locator(".mx-grades-page")
playwright.expect(g.locator(".mx-glist-new")).to_contain_text("הערה חדשה מהמורה")
playwright.expect(g.locator(".mx-glist-units")).to_contain_text("טכניקה אלגברית 1 62") # the unit's grade, on one line
rows = g.locator(".mx-glist-row")
playwright.expect(rows).to_have_count(4) # one row each, newest first
playwright.expect(rows.nth(0)).to_contain_text("בוחן מסכם")
playwright.expect(rows.nth(0)).to_contain_text("62/100")
playwright.expect(rows.nth(0)).to_contain_text("★ ציון היחידה")
playwright.expect(rows.nth(0)).to_contain_text("💬") # the new comment is on the final
playwright.expect(rows.nth(1)).to_contain_text("שיעורי בית")
playwright.expect(rows.nth(1)).to_contain_text("90/100")
playwright.expect(rows.nth(1).locator("[data-ws-open]")).to_have_text("צפייה")
playwright.expect(rows.nth(2)).to_contain_text("ממתין לבדיקה · שאלה אחת")
playwright.expect(rows.nth(2).locator('[data-uquiz="t"]')).to_have_text("הגשה מחדש") # a בוחן חיזוק: again
playwright.expect(rows.nth(3)).to_contain_text("85/100")
playwright.expect(rows.nth(3)).to_contain_text("הכי טוב")
playwright.expect(rows.nth(0).locator("[data-uquiz]")).to_have_count(0) # a בוחן מסכם is handed in once
rows.nth(0).locator('[data-grade-sitting="t:f1"]').click()
playwright.expect(page.locator(".mx-uq-sheet h3")).to_have_text("בוחן מסכם: 62/100", timeout=10000)
assert page.evaluate("""() => { const k = Object.keys(localStorage).find(k => k.startsWith('mathly.progress.v1:'));
return JSON.parse(localStorage.getItem(k)).uq.t.history.some(h => h.sid === 'f1' && h.mode === 'final'); }""")
assert not errors, errors
finally:
browser.close()
def test_homework_not_handed_in_yet_is_first_in_the_grades_with_a_hand_in_button(task_server):
import httpx
from test_focus_browser import session
url, app = task_server
with httpx.Client(base_url=url, headers={"Authorization": f"Bearer {TEACHER}"}) as teacher:
wid = teacher.post("/api/classes/3/worksheets", data={"title": "דף"}, files=photos(1)).json()["id"]
teacher.post("/api/classes/3/assignments", json={"title": "ש״ב", "due_on": "2099-11-12", "items": [{"kind": "worksheet", "id": wid}]})
empty = {"units": [], "quizzes_known": True, "comments": [], "worksheets": []}
with playwright.sync_playwright() as p:
browser = p.chromium.launch()
try:
page, errors = session(browser, STUDENT)
page.route("**/api/math/my-grades", lambda route: route.fulfill(json=empty))
page.goto(url)
page.locator('.mx-nav [data-view="home"]').click()
playwright.expect(page.locator(".mx-asgs")).to_be_visible() # the assignments are known
page.locator('.mx-nav [data-view="grades"]').click()
row = page.locator(".mx-grades-page .mx-glist-row.todo")
playwright.expect(row).to_contain_text("שיעורי בית")
playwright.expect(row).to_contain_text("עד 12.11")
row.locator("[data-ws-open]").click()
playwright.expect(page.locator(".mx-ws-sheet")).to_be_visible()
assert not errors, errors
finally:
browser.close()