Download tests/test_worksheets_browser.py from wzender/mathly: direct link, hf CLI and curl.
- Browser
- Download file 28 kB
-
https://huggingface.co/spaces/wzender/mathly/resolve/main/tests/test_worksheets_browser.py
- Command line
-
hf download hf://spaces/wzender/mathly/tests/test_worksheets_browser.py
-
curl -L -o test_worksheets_browser.py https://huggingface.co/spaces/wzender/mathly/resolve/main/tests/test_worksheets_browser.py
28 kB
| """Worksheets end to end: a real server (rows and files in memory) and a real browser for the teacher and the student. | |
| The teacher attaches a PDF to an assignment, a student opens it and hands in photos of a solution, the teacher checks it | |
| with a score and a comment, and the student sees the check. Then the teacher removes the worksheet, after confirming.""" | |
| import io | |
| import re | |
| import socket | |
| import threading | |
| import time | |
| import urllib.parse | |
| import pytest | |
| from PIL import Image | |
| from app.main import create_app | |
| from test_assignments import MemoryAssignments | |
| from test_worksheets import MemoryWorksheets, pdf | |
| playwright = pytest.importorskip("playwright.sync_api") | |
| TEACHER, STUDENT = "aaaaaaaa-0000-4000-8000-000000000001", "aaaaaaaa-0000-4000-8000-000000000002" | |
| mail = {TEACHER: "teacher@test.invalid", STUDENT: "dana@test.invalid"} | |
| class SchoolAuth: | |
| url, key, secret = "https://test.invalid", "public-test", "server-test" | |
| teachers = developers = frozenset() | |
| def user(self, token): | |
| return {"id": token, "email": mail[token]} if token in mail else None | |
| def classes(self, fresh=False): | |
| return [{"id": 3, "name": "ืืณ1", "lessons": ["t", "k"], "teachers": [mail[TEACHER]], | |
| "emails": list(mail.values()), "nicknames": {}, "exams": []}] | |
| def access_for(self, _id, **kwargs): | |
| return None | |
| def user_accounts(self, emails): | |
| return {email: {"id": uid, "name": ""} for uid, email in mail.items() if email in emails} | |
| def _admin(self, path, method="GET", body=None, prefer=""): | |
| from app.auth import AuthUnavailable | |
| if path.startswith("/rest/v1/math_progress"): | |
| return [] | |
| raise AuthUnavailable("supabase answered 404: no other tables here") | |
| class FakeGrader: | |
| """The AI's check of a homework submission: a fixed suggestion.""" | |
| def __init__(self): | |
| self.calls = [] | |
| def grade(self, title, sheet_pages, solution_pages): | |
| self.calls.append((title, len(sheet_pages), len(solution_pages))) | |
| return {"readable": True, "score": 70, "comment": "ืืืขื! ืฉืืื ืื ืืชืจืืื 2", "usage": None} | |
| class NoAnswers: | |
| configured = False | |
| def quiz(self, *args, **kwargs): | |
| from app.answer_scans import ScanError | |
| raise ScanError(503, "not_here") | |
| review = quiz | |
| def photo_file(tmp_path, name, color): | |
| path = tmp_path / name | |
| Image.new("RGB", (600, 800), color).save(path, "JPEG") | |
| return str(path) | |
| def session(browser, uid): | |
| ctx = browser.new_context(viewport={"width": 390, "height": 844}, is_mobile=True, has_touch=True) | |
| ctx.route("https://cdn.jsdelivr.net/**", lambda route: route.abort()) | |
| ctx.add_init_script('''window.supabase={createClient:()=>({auth:{ | |
| getSession:async()=>({data:{session:{access_token:"%s",user:{id:"%s"}}}}), | |
| onAuthStateChange:()=>({data:{subscription:{unsubscribe(){}}}}),signOut:async()=>{} | |
| }})};''' % (uid, uid)) | |
| page = ctx.new_page() | |
| errors = [] | |
| page.on("pageerror", lambda e: errors.append(str(e))) | |
| return page, errors | |
| def school(monkeypatch): | |
| monkeypatch.setenv("MATHLY_OPEN_ENDED_LOCKED", "0") # the AI check of homework follows the open-ended lock | |
| app = create_app(auth=SchoolAuth()) | |
| app.state.homework_grader = FakeGrader() | |
| app.state.assignments, app.state.worksheets, app.state.answers = MemoryAssignments(), MemoryWorksheets(), NoAnswers() | |
| 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 uvicorn(): | |
| import uvicorn as u | |
| return u | |
| def open_tasks(page, url): | |
| page.goto(url) | |
| page.locator('[data-act="class-report"]').click() | |
| sheet = page.locator(".mx-report-sheet") | |
| sheet.locator("[data-report-tab=tasks]").click() | |
| return sheet | |
| def test_a_worksheet_from_upload_to_hand_in_to_check(school, tmp_path): | |
| url, app = school | |
| store = app.state.worksheets | |
| sheet_pdf = tmp_path / "pythagoras.pdf" | |
| sheet_pdf.write_bytes(pdf(2)) | |
| with playwright.sync_playwright() as p: | |
| browser = p.chromium.launch() | |
| try: | |
| # ---- the teacher attaches a PDF to a new assignment | |
| teacher, t_errors = session(browser, TEACHER) | |
| sheet = open_tasks(teacher, url) | |
| sheet.locator("[data-asg-new]").click() | |
| form = sheet.locator("[data-asg-form]") | |
| form.locator("[name=title]").fill("ืฉืืขืืจื ืืืช") | |
| form.locator("[name=due_on]").fill("2099-01-01") | |
| status = form.locator("[data-ws-status]") | |
| form.locator("[data-ws-upload]").click() | |
| playwright.expect(status).to_have_text("ืืชืื ืฉื ืืฉืืขืืจื ืืืืช.") | |
| form.locator("[name=ws_title]").fill("ืฉืืขืืจื ืืืช โ ืคืืชืืืจืก") | |
| form.locator("[name=ws_files]").set_input_files(str(sheet_pdf)) | |
| form.locator("[data-ws-upload]").click() | |
| playwright.expect(form.locator("[data-ws-list] li")).to_have_count(1) | |
| playwright.expect(form.locator("[data-ws-list]")).to_contain_text("2 ืขืืณ ยท PDF") | |
| playwright.expect(status).to_contain_text("ืืืขืื") | |
| form.locator("button[type=submit]").click() | |
| card = sheet.locator(".mx-asg-t") | |
| playwright.expect(card).to_contain_text("ืฉืืขืืจื ืืืช: ืฉืืขืืจื ืืืช โ ืคืืชืืืจืก") | |
| playwright.expect(card.locator(".mx-ws-sum")).to_contain_text("ืืืืฉื 0 ืืชืื 1 ยท 1 ืื ืืืืฉื ยท 0 ืืืชืื ืื ืืืืืงื") | |
| (assignment,) = app.state.assignments.rows.values() | |
| (wid,) = store.rows | |
| assert assignment["items"] == [{"kind": "worksheet", "id": wid}] | |
| # ---- the student opens it and hands in two photos | |
| student, s_errors = session(browser, STUDENT) | |
| student.goto(url) | |
| student.locator('.mx-nav [data-view="home"]').click() | |
| asg = student.locator(".mx-asgs .mx-asg") | |
| playwright.expect(asg).to_contain_text("ืฉืืขืืจื ืืืช: ืฉืืขืืจื ืืืช โ ืคืืชืืืจืก") | |
| playwright.expect(asg).to_contain_text("0 ืืชืื 1 ืืืฉืืื") | |
| asg.locator("[data-asg-go]").click() # the continue button opens it | |
| ws = student.locator(".mx-ws-sheet") | |
| playwright.expect(ws.locator("#mxTitle")).to_have_text("ืฉืืขืืจื ืืืช โ ืคืืชืืืจืก") | |
| playwright.expect(ws.locator(".mx-ws-pages img")).to_have_count(2) | |
| student.wait_for_function("[...document.querySelectorAll('.mx-ws-pages img')].every(i => i.complete && i.naturalWidth > 0)") | |
| with student.expect_download() as got: | |
| ws.locator("[data-ws-download]").click() | |
| assert got.value.suggested_filename == "ืฉืืขืืจื ืืืช โ ืคืืชืืืจืก.pdf" | |
| ws.locator("[data-ws-file]").set_input_files([photo_file(tmp_path, "a.jpg", "red"), photo_file(tmp_path, "b.jpg", "blue")]) | |
| playwright.expect(ws.locator(".mx-ws-chosen li")).to_have_count(2) | |
| ws.locator('[data-ws-up="1"]').click() # blue first now | |
| # a page written on the board joins the hand-in like a photo (then it is dropped again: two pages go in) | |
| ws.locator("[data-ws-board]").click() | |
| board = student.locator(".mx-board.full") | |
| playwright.expect(board).to_be_visible() | |
| box = board.locator("canvas").bounding_box() | |
| student.mouse.move(box["x"] + 60, box["y"] + 60) | |
| student.mouse.down(); student.mouse.move(box["x"] + 200, box["y"] + 160, steps=8); student.mouse.up() | |
| board.locator(".mx-board-send").click() | |
| playwright.expect(ws.locator(".mx-ws-chosen li")).to_have_count(3) | |
| playwright.expect(board).to_have_count(0) | |
| ws.locator('[data-ws-drop="2"]').click() | |
| playwright.expect(ws.locator(".mx-ws-chosen li")).to_have_count(2) | |
| ws.locator("[data-ws-send]").click() | |
| playwright.expect(ws.locator(".mx-ws-done")).to_contain_text("ืืืืฉ") | |
| playwright.expect(ws).to_contain_text("ืืืืฉื ื ืฉืืื ืืืืจื") | |
| (sub,) = store.subs.values() | |
| assert len(sub["page_keys"]) == 2 | |
| with Image.open(io.BytesIO(store.files[sub["page_keys"][0]][0])) as first: | |
| assert first.getpixel((10, 10))[2] > 150 # the reordered page went first | |
| ws.locator(".mx-x").click() | |
| playwright.expect(student.locator(".mx-asgs .mx-asg")).to_have_count(0) # done: the only item | |
| playwright.expect(student.locator(".mx-asg-done summary")).to_have_text("ืืืฉืืื (1)") | |
| # ---- the teacher checks it | |
| sheet = open_tasks(teacher, url) | |
| card = sheet.locator(".mx-asg-t") | |
| playwright.expect(card.locator(".mx-ws-sum")).to_contain_text("ืืืืฉื 1 ืืชืื 1 ยท 0 ืื ืืืืฉื ยท 1 ืืืชืื ืื ืืืืืงื") | |
| # the ืืืฉืืช tab lists it by student and kind, and opens it for checking | |
| sheet.locator("[data-report-tab=subs]").click() | |
| dana = sheet.locator(".mx-report-student", has_text="dana") | |
| playwright.expect(dana).to_contain_text("1 ืืืชืื ืืช ืืืืืงื") | |
| playwright.expect(dana.locator("h5")).to_have_text(["ืฉืืขืืจื ืืืช", "ืืื ืื", "ืืืื ื ืชืจืืื ยท ืืืืืฉืืจ"]) | |
| playwright.expect(dana.locator("section").first).to_contain_text("ืฉืืขืืจื ืืืช โ ืคืืชืืืจืก") | |
| playwright.expect(dana).to_contain_text("ืืืื ืื ืืื ื ืืืื ืื ืืจืืข") # the test server has no quiz sittings | |
| playwright.expect(dana).to_contain_text("ืขืื ืื ื ืขืฉื ืืืื ืชืจืืื") | |
| dana.locator("[data-ws-sub]").click() | |
| review = sheet.locator(".mx-ws-review") | |
| playwright.expect(review).to_contain_text("dana") | |
| playwright.expect(review.locator(".mx-ws-pages img")).to_have_count(2) | |
| teacher.wait_for_function("[...document.querySelectorAll('.mx-ws-review img')].every(i => i.complete && i.naturalWidth > 0)") | |
| check = review.locator("[data-ws-check]") | |
| review.locator("[data-ws-ai]").click() # the AI suggests a score and a comment | |
| playwright.expect(check.locator("[name=score]")).to_have_value("70") | |
| playwright.expect(check.locator("[name=comment]")).to_have_value("ืืืขื! ืฉืืื ืื ืืชืจืืื 2") | |
| playwright.expect(review.locator("[data-ws-ai-status]")).to_contain_text("ืืฆืขืช ืึพAI ืืืื ืกื ืืืืคืก") | |
| assert app.state.homework_grader.calls == [("ืฉืืขืืจื ืืืช โ ืคืืชืืืจืก", 2, 2)] # the sheet's pages, the solution's | |
| assert not list(store.subs.values())[0]["checked_at"] # a suggestion is not saved | |
| check.locator("[name=score]").fill("120") | |
| check.locator("button[type=submit]").click() | |
| playwright.expect(check.locator("[role=status]")).to_contain_text("ืืื 0 ืึพ100") | |
| check.locator("[name=score]").fill("85") | |
| check.locator("[name=comment]").fill("ืืคื, ืฉืืื ืื ืืืืืืืช") | |
| check.locator("button[type=submit]").click() | |
| playwright.expect(sheet).to_contain_text("ืืืืืงื ื ืฉืืจื") | |
| playwright.expect(sheet.locator(".mx-report-student", has_text="dana")).to_contain_text("ื ืืืง, ืฆืืื 85") | |
| sheet.locator("[data-report-tab=tasks]").click() | |
| playwright.expect(sheet.locator(".mx-ws-sum")).to_contain_text("0 ืืืชืื ืื ืืืืืงื") | |
| playwright.expect(sheet.locator("[data-ws-sub]")).to_have_text("โ 85") | |
| (sub,) = store.subs.values() | |
| assert (sub["score"], sub["comment"]) == (85, "ืืคื, ืฉืืื ืื ืืืืืืืช") and sub["checked_at"] | |
| # ---- the student sees the check, and cannot hand in again | |
| student.reload() | |
| student.locator('.mx-nav [data-view="home"]').click() | |
| done = student.locator(".mx-asg-done") | |
| done.locator("summary").click() | |
| playwright.expect(done.locator(".mx-ws-badge")).to_have_text("ื ืืืง ยท 85") | |
| playwright.expect(done.locator(".mx-ws-comment")).to_contain_text("ืืคื, ืฉืืื ืื ืืืืืืืช") | |
| done.locator("[data-ws-open]").click() | |
| playwright.expect(ws.locator(".mx-ws-checked")).to_contain_text("ืฆืืื 85") | |
| assert ws.locator("[data-ws-pick]").count() == 0 | |
| # ---- the teacher removes the worksheet: confirmed, with how many handed in | |
| asked = [] | |
| teacher.once("dialog", lambda d: (asked.append(d.message), d.accept())) | |
| sheet = open_tasks(teacher, url) | |
| sheet.locator("[data-asg-edit]").click() | |
| form = sheet.locator("[data-asg-form]") | |
| assert form.locator("[name=skill], [name=quiz], [name=exam]").count() == 0 # a teacher gives worksheets only | |
| form.locator(f'[data-ws-remove="{wid}"]').click() | |
| assert asked and "1 ืชืืืืืื ืืืจ ืืืืฉื" in asked[0] | |
| playwright.expect(form.locator("[data-ws-list] li")).to_have_count(0) | |
| form.locator("button[type=submit]").click() | |
| playwright.expect(form.locator(":scope > .mx-row > [role=status]")).to_have_text("ืืขืื ืฉืืขืืจื ืืืช ืืื ืืคืืืช.") | |
| form.locator("[name=ws_title]").fill("ืฉืืขืืจื ืืืช ืืืฉืื") # another worksheet in its place | |
| form.locator("[name=ws_files]").set_input_files(photo_file(tmp_path, "new.jpg", "green")) | |
| form.locator("[data-ws-upload]").click() | |
| playwright.expect(form.locator("[data-ws-list] li")).to_have_count(1) | |
| form.locator("button[type=submit]").click() | |
| playwright.expect(sheet.locator(".mx-asg-t")).not_to_contain_text("ืคืืชืืืจืก") | |
| playwright.expect(sheet.locator(".mx-asg-t")).to_contain_text("ืฉืืขืืจื ืืืช ืืืฉืื") | |
| assert wid not in store.rows and not store.subs and len(store.rows) == 1 | |
| assert not t_errors and not s_errors | |
| finally: | |
| browser.close() | |
| def test_without_worksheets_the_form_has_no_upload(school): | |
| url, app = school | |
| app.state.worksheets.fail = True | |
| with playwright.sync_playwright() as p: | |
| browser = p.chromium.launch() | |
| try: | |
| teacher, errors = session(browser, TEACHER) | |
| sheet = open_tasks(teacher, url) | |
| sheet.locator("[data-asg-new]").click() | |
| playwright.expect(sheet.locator("[data-asg-form]")).to_be_visible() | |
| assert sheet.locator(".mx-ws-field").count() == 0 | |
| assert not errors | |
| finally: | |
| browser.close() | |
| def test_the_teacher_sees_their_classes_on_the_home_screen(school): | |
| url, _ = school | |
| with playwright.sync_playwright() as p: | |
| browser = p.chromium.launch() | |
| try: | |
| teacher, errors = session(browser, TEACHER) | |
| teacher.goto(url) | |
| card = teacher.locator(".mx-classes") | |
| playwright.expect(card.locator("h2")).to_have_text("ืืืืชืืช ืฉืื") | |
| playwright.expect(card.locator(".mx-myclass")).to_have_count(1) | |
| playwright.expect(card.locator(".mx-myclass")).to_contain_text("ืืณ1") | |
| playwright.expect(card.locator(".mx-myclass")).to_contain_text("1 ืชืืืืืื") # dana; the teacher is not counted | |
| playwright.expect(card.locator(".mx-ai-left")).to_contain_text("3 ืืงืฉืืช AI") | |
| card.locator(".mx-myclass").click() | |
| playwright.expect(teacher.locator(".mx-report-sheet")).to_contain_text("dana") # the class's students | |
| student, s_errors = session(browser, STUDENT) | |
| student.goto(url) | |
| playwright.expect(student.locator(".mx-subject-grid, .mx-continue").first).to_be_visible() | |
| assert student.locator(".mx-classes").count() == 0 | |
| assert not errors and not s_errors | |
| finally: | |
| browser.close() | |
| def test_a_worksheet_returned_for_a_fix_is_handed_in_again_with_a_note(school, tmp_path): | |
| import httpx | |
| url, app = school | |
| store = app.state.worksheets | |
| headers = {"Authorization": f"Bearer {TEACHER}"} | |
| with httpx.Client(base_url=url, headers=headers) as api: | |
| wid = api.post("/api/classes/3/worksheets", data={"title": "ืืฃ ืคืืืื"}, | |
| files=[("files", ("a.jpg", open(photo_file(tmp_path, "s.jpg", "white"), "rb").read(), "image/jpeg"))]).json()["id"] | |
| assert api.post("/api/classes/3/assignments", json={"title": "ืฉืืขืืจื ืืืช", "due_on": "2099-01-01", | |
| "items": [{"kind": "worksheet", "id": wid}]}).status_code == 201 | |
| with playwright.sync_playwright() as p: | |
| browser = p.chromium.launch() | |
| try: | |
| student, s_errors = session(browser, STUDENT) | |
| student.goto(url) | |
| student.locator('.mx-nav [data-view="home"]').click() | |
| student.locator(".mx-asgs [data-asg-go]").click() | |
| ws = student.locator(".mx-ws-sheet") | |
| ws.locator("[data-ws-file]").set_input_files([photo_file(tmp_path, "a.jpg", "red")]) | |
| ws.locator("[data-ws-note]").fill("ืื ืืื ืชื ืืช ืฉืืื 3") | |
| ws.locator("[data-ws-send]").click() | |
| playwright.expect(ws.locator("[role=status]").first).to_contain_text("ืืืชืื ืืืืืงื") | |
| playwright.expect(ws).to_contain_text("ืงืืื: 1 ืขืืณ") | |
| playwright.expect(ws).to_contain_text("ืืืขืจื ืฉืื ืืืืจื: ืื ืืื ืชื ืืช ืฉืืื 3") | |
| teacher, t_errors = session(browser, TEACHER) | |
| sheet = open_tasks(teacher, url) | |
| sheet.locator("[data-report-tab=subs]").click() | |
| sheet.locator(".mx-report-student", has_text="dana").locator("[data-ws-sub]").click() | |
| review = sheet.locator(".mx-ws-review") | |
| playwright.expect(review.locator(".mx-ws-note-shown")).to_contain_text("ืื ืืื ืชื ืืช ืฉืืื 3") | |
| review.locator(".mx-ws-return-box summary").click() | |
| review.locator("[data-ws-return] button[type=submit]").click() | |
| playwright.expect(review.locator("[data-ws-return] [role=status]")).to_have_text("ืืชืื ืื ืืชืงื.") | |
| review.locator("[data-ws-return] [name=comment]").fill("ืืกืจื ืืจื ืคืชืจืื ืืฉืืื 2") | |
| review.locator("[data-ws-return] button[type=submit]").click() | |
| playwright.expect(sheet.locator(".mx-report")).to_contain_text("ืืืืฉื ืืืืืจื ืืชืืงืื") | |
| (sub,) = store.subs.values() | |
| assert sub["state"] == "returned" and sub["return_comment"] == "ืืกืจื ืืจื ืคืชืจืื ืืฉืืื 2" | |
| student.reload() | |
| student.locator('.mx-nav [data-view="home"]').click() | |
| card = student.locator(".mx-asgs .mx-asg") | |
| playwright.expect(card.locator(".mx-ws-badge")).to_have_text("โฉ ืืืืืจ ืืชืืงืื") | |
| playwright.expect(card).to_contain_text("ืืกืจื ืืจื ืคืชืจืื ืืฉืืื 2") | |
| card.locator("[data-ws-open]").click() | |
| ws = student.locator(".mx-ws-sheet") | |
| playwright.expect(ws.locator(".mx-ws-returned")).to_contain_text("ืืืืืจ ืืชืืงืื") | |
| ws.locator("[data-ws-file]").set_input_files([photo_file(tmp_path, "b.jpg", "blue"), photo_file(tmp_path, "c.jpg", "green")]) | |
| playwright.expect(ws.locator("[data-ws-send]")).to_contain_text("ืืืฉื ืืชืืงื ืช") | |
| ws.locator("[data-ws-send]").click() | |
| playwright.expect(ws).to_contain_text("ืืจืกื 2") | |
| playwright.expect(ws.locator(".mx-ws-earlier summary")).to_have_text("ืืืฉืืช ืงืืืืืช (1)") | |
| assert sorted(s["version"] for s in store.subs.values()) == [1, 2] | |
| sheet = open_tasks(teacher, url) | |
| playwright.expect(sheet.locator(".mx-ws-sum")).to_contain_text("1 ืืืชืื ืื ืืืืืงื") | |
| assert not (s_errors or t_errors), (s_errors, t_errors) | |
| finally: | |
| browser.close() | |
| def test_a_page_photographed_on_the_phone_joins_the_hand_in_and_goes_with_the_note(school, tmp_path): | |
| """The computer asks the phone (the answer-scan flow, answered here by routes) and the photo it gets back joins the | |
| worksheet's hand-in, sent with the student's note.""" | |
| import json | |
| import httpx | |
| url, app = school | |
| store = app.state.worksheets | |
| with httpx.Client(base_url=url, headers={"Authorization": f"Bearer {TEACHER}"}) as api: | |
| wid = api.post("/api/classes/3/worksheets", data={"title": "ืืฃ"}, | |
| files=[("files", ("a.jpg", open(photo_file(tmp_path, "s.jpg", "white"), "rb").read(), "image/jpeg"))]).json()["id"] | |
| api.post("/api/classes/3/assignments", json={"title": "ืฉืดื", "due_on": "2099-01-01", "items": [{"kind": "worksheet", "id": wid}]}) | |
| page_jpg = open(photo_file(tmp_path, "phone.jpg", "purple"), "rb").read() | |
| aid, sid = "cccccccc-0000-4000-8000-000000000001", "cccccccc-0000-4000-8000-000000000002" | |
| asked, polls = [], {"n": 0} | |
| def attempt(route): | |
| asked.append(json.loads(route.request.post_data)) | |
| route.fulfill(json={"attempt": {"id": aid, "question_id": f"worksheet:{wid}"}, "scan": None, "attachment": None}) | |
| def scan(route): | |
| route.fulfill(json={"attempt": {"id": aid}, "scan": {"id": sid, "state": "waiting", "code": "1234"}, "attachment": None}) | |
| def poll(route): | |
| polls["n"] += 1 # the phone sends the photo by the second look | |
| done = polls["n"] >= 2 | |
| route.fulfill(json={"attempt": {"id": aid}, "scan": {"id": sid, "state": "received" if done else "waiting", "code": "1234"}, | |
| "attachment": {"id": "x"} if done else None}) | |
| with playwright.sync_playwright() as p: | |
| browser = p.chromium.launch() | |
| try: | |
| ctx = browser.new_context(viewport={"width": 1280, "height": 900}) | |
| ctx.route("https://cdn.jsdelivr.net/**", lambda route: route.abort()) | |
| ctx.add_init_script('''window.supabase={createClient:()=>({auth:{ | |
| getSession:async()=>({data:{session:{access_token:"%s",user:{id:"%s"}}}}), | |
| onAuthStateChange:()=>({data:{subscription:{unsubscribe(){}}}}),signOut:async()=>{}}})};''' % (STUDENT, STUDENT)) | |
| page = ctx.new_page() | |
| errors = [] | |
| page.on("pageerror", lambda e: errors.append(str(e))) | |
| page.route("**/api/me", lambda route: route.fulfill(json={**route.fetch().json(), "answer_scans": True})) | |
| page.route("**/api/math/worksheet-attempts", attempt) | |
| page.route(f"**/api/math/answer-attempts/{aid}/scans", scan) | |
| page.route(f"**/api/math/answer-attempts/{aid}/image", lambda route: route.fulfill(body=page_jpg, content_type="image/jpeg")) | |
| page.route(f"**/api/math/answer-attempts/{aid}", poll) | |
| page.goto(url) | |
| page.locator('.mx-nav [data-view="home"]').click() | |
| page.locator(".mx-asgs [data-asg-go]").click() | |
| ws = page.locator(".mx-ws-sheet") | |
| ws.locator("[data-ws-phone]").click() | |
| playwright.expect(ws.locator(".mx-ws-chosen li")).to_have_count(1, timeout=10000) | |
| playwright.expect(ws.locator(".mx-res-msg")).to_have_text("ืืชืืื ื ืืืืืคืื ื ืืกืคื ืืืืฉื.") | |
| assert asked == [{"worksheet_id": wid, "new": True}] # the page asked for this worksheet's page | |
| ws.locator("[data-ws-note]").fill("ืฆืืืืชื ืืืืืคืื") | |
| ws.locator("[data-ws-send]").click() | |
| playwright.expect(ws).to_contain_text("ืืืืฉื ื ืฉืืื ืืืืจื") | |
| (sub,) = store.subs.values() | |
| assert sub["worksheet_id"] == wid and sub["note"] == "ืฆืืืืชื ืืืืืคืื" and len(sub["page_keys"]) == 1 | |
| assert not errors, errors | |
| finally: | |
| browser.close() | |
| def test_a_page_from_the_board_is_added_and_the_send_button_is_shown(school, tmp_path, width, height, mobile): | |
| """On a computer the board opens beside the worksheet (its pages stay in view); on a phone, full screen. A page | |
| added from the board is not handed in yet: the hand-in comes into view with its send button, which hands it in.""" | |
| import httpx | |
| url, app = school | |
| with httpx.Client(base_url=url, headers={"Authorization": f"Bearer {TEACHER}"}) as api: | |
| wid = api.post("/api/classes/3/worksheets", data={"title": "ืืฃ"}, | |
| files=[("files", ("a.jpg", open(photo_file(tmp_path, "s.jpg", "white"), "rb").read(), "image/jpeg"))]).json()["id"] | |
| api.post("/api/classes/3/assignments", json={"title": "ืฉืดื", "due_on": "2099-01-01", "items": [{"kind": "worksheet", "id": wid}]}) | |
| with playwright.sync_playwright() as p: | |
| browser = p.chromium.launch() | |
| try: | |
| ctx = browser.new_context(viewport={"width": width, "height": height}, is_mobile=mobile, has_touch=mobile) | |
| ctx.route("https://cdn.jsdelivr.net/**", lambda route: route.abort()) | |
| ctx.add_init_script('''window.supabase={createClient:()=>({auth:{ | |
| getSession:async()=>({data:{session:{access_token:"%s",user:{id:"%s"}}}}), | |
| onAuthStateChange:()=>({data:{subscription:{unsubscribe(){}}}}),signOut:async()=>{}}})};''' % (STUDENT, STUDENT)) | |
| page = ctx.new_page() | |
| errors = [] | |
| page.on("pageerror", lambda e: errors.append(str(e))) | |
| page.goto(url) | |
| page.locator('.mx-nav [data-view="home"]').click() | |
| page.locator(".mx-asgs [data-asg-go]").click() | |
| page.locator("[data-ws-board]").click() | |
| board = page.locator(".mx-board") | |
| playwright.expect(board).to_be_visible() | |
| if mobile: | |
| playwright.expect(board).to_have_class(re.compile(r"\bfull\b")) | |
| else: | |
| playwright.expect(board).not_to_have_class(re.compile(r"\bfull\b")) | |
| playwright.expect(page.locator(".mx-ws-sheet.with-board .mx-ws-pages")).to_be_visible() # the exercises stay in view | |
| box = board.locator("canvas").first.bounding_box() | |
| page.mouse.move(box["x"] + 60, box["y"] + 60) | |
| page.mouse.down(); page.mouse.move(box["x"] + 200, box["y"] + 160, steps=8); page.mouse.up() | |
| board.locator(".mx-board-send").click() | |
| send = page.locator("[data-ws-send]") | |
| playwright.expect(send).to_be_focused() | |
| playwright.expect(send).to_be_in_viewport() | |
| playwright.expect(page.locator(".mx-ws-sheet .mx-res-msg")).to_contain_text('ืืืฆื ืขื "ืืืฉื"') | |
| assert not app.state.worksheets.subs # added, not handed in yet | |
| send.click() | |
| playwright.expect(page.locator(".mx-ws-sheet")).to_contain_text("ืืืืฉื ื ืฉืืื ืืืืจื") | |
| assert len(app.state.worksheets.subs) == 1 and not errors, errors | |
| finally: | |
| browser.close() | |