Download mathly2/tests/test_boardwork_browser.py from wzender/mathly: direct link, hf CLI and curl.
- Browser
- Download file 11.8 kB
-
https://huggingface.co/spaces/wzender/mathly/resolve/main/mathly2/tests/test_boardwork_browser.py
- Command line
-
hf download hf://spaces/wzender/mathly/mathly2/tests/test_boardwork_browser.py
-
curl -L -o test_boardwork_browser.py https://huggingface.co/spaces/wzender/mathly/resolve/main/mathly2/tests/test_boardwork_browser.py
11.8 kB
| """The work on the board in a real browser: a quiz asks for the way before a choice, the board goes with the answer, and the teacher gives | |
| points for the way (board-work 2.1-2.3).""" | |
| import time | |
| import uuid | |
| import pytest | |
| playwright = pytest.importorskip("playwright.sync_api") | |
| from mathly2.bank import load_bank | |
| from mathly2.config import Settings | |
| from mathly2.main import create_app | |
| from mathly2.store import InMemoryStore | |
| from mathly2.tests.serve import offline_formulas, running | |
| from mathly2.tests.test_home_browser import KID, PHONE, TEACHER, sign_in, teacher_page | |
| KID_ID = str(uuid.uuid5(uuid.NAMESPACE_DNS, KID)) | |
| def bank(): | |
| return load_bank() | |
| def browser(): | |
| with playwright.sync_playwright() as p: | |
| b = p.chromium.launch() | |
| yield b | |
| b.close() | |
| def draw_line(page): | |
| box = page.locator(".mx-board canvas").bounding_box() | |
| x, y = box["x"] + 60, box["y"] + 160 | |
| page.mouse.move(x, y) | |
| page.mouse.down() | |
| page.mouse.move(x + 60, y + 30, steps=5) | |
| page.mouse.move(x + 120, y + 60, steps=5) | |
| page.mouse.up() | |
| def wait_until(check, seconds=10): | |
| end = time.time() + seconds | |
| while time.time() < end: | |
| if check(): | |
| return | |
| time.sleep(0.1) | |
| raise AssertionError("timed out") | |
| def test_a_quiz_asks_for_the_way_first_and_the_teacher_gives_points_for_it(browser, bank): | |
| lesson = next(l["id"] for l in bank.lessons if any(s["unit"] == l["id"] and bank.question_keys(s["id"]) for s in bank.skills)) | |
| store = InMemoryStore() | |
| klass = store.create_class("setup", "A", lessons=[lesson], teacher_email=TEACHER) | |
| store.add_member(klass["id"], KID) | |
| app = create_app(Settings(teachers=frozenset({TEACHER})), bank=bank, store=store) | |
| with running(app) as url: | |
| page = sign_in(browser, url, f"#/quiz/{lesson}") | |
| page.wait_for_selector("#qz-q") | |
| page.locator(".qz-choice").first.click() | |
| page.wait_for_selector("#qz-need-work") | |
| assert page.locator(".qz-choice.picked").count() == 0 # no choice without the way | |
| page.click("#qz-open-board") | |
| page.wait_for_selector(".mx-board.full canvas") | |
| draw_line(page) | |
| page.click(".mx-board .mx-board-back") | |
| page.locator(".qz-choice").first.click() | |
| page.wait_for_selector(".qz-choice.picked") | |
| attempt = store.attempts_of(KID_ID, "quiz")[0] | |
| key = attempt["questions"][0]["key"] | |
| wait_until(lambda: key in store.work_of(KID_ID, attempt["id"])) # the board went with the answer | |
| page.click("#qz-submit") | |
| page.click("#qz-yes") | |
| page.wait_for_selector("#qz-grade") | |
| assert page.errors == [] | |
| teacher = teacher_page(browser, url) | |
| teacher.click("#teacher-tabs [data-tab='work']") | |
| teacher.wait_for_selector(f".work-list li[data-attempt='{attempt['id']}']") | |
| assert "诇讘讚讬拽讛" in teacher.inner_text(".work-list li") | |
| teacher.click(f"[data-open='{attempt['id']}']") | |
| teacher.wait_for_selector(f".work-q[data-key='{key}'] img.work-pic:not([hidden])") # the student's board, through the server | |
| assert teacher.locator(".work-q .no-work").count() == len(attempt["questions"]) - 1 | |
| before = int(teacher.inner_text("#work-grade")) | |
| second = attempt["questions"][1]["key"] # not answered: half the points for the way | |
| teacher.click(f".work-q[data-key='{second}'] [data-points='0.5']") | |
| teacher.click("#work-save") | |
| teacher.wait_for_selector("#work-check .ex-note") | |
| after = int(teacher.inner_text("#work-grade")) | |
| assert after > before and store.get_attempt(attempt["id"])["credit"] == {second: 0.5} | |
| teacher.click("#work-back") | |
| teacher.wait_for_selector(".work-list li") | |
| assert "谞讘讚拽" in teacher.inner_text(".work-list li") | |
| assert teacher.errors == [] | |
| def test_a_lesson_round_needs_no_board_but_sends_it_when_there_is_one(browser, bank): | |
| lesson = next(l["id"] for l in bank.lessons if any(s["unit"] == l["id"] and bank.question_keys(s["id"]) for s in bank.skills)) | |
| store = InMemoryStore() | |
| store.add_member(store.create_class("setup", "A", lessons=[lesson], teacher_email=TEACHER)["id"], KID) | |
| with running(create_app(Settings(), bank=bank, store=store)) as url: | |
| page = sign_in(browser, url, f"#/round/{lesson}") | |
| page.wait_for_selector(".round-layer #ex-q") | |
| page.locator(".ex-choice").first.click() # no board: the answer goes through | |
| page.wait_for_selector(".ex-ok, .ex-bad") | |
| page.click("#rd-next") | |
| page.wait_for_function("document.querySelector('#rd-count').textContent.includes('2 诪转讜讱')") | |
| key = page.locator("#ex-q").get_attribute("data-key") | |
| page.click("[data-board-toggle]") | |
| page.wait_for_selector(".mx-board.full canvas") | |
| draw_line(page) | |
| page.click(".mx-board .mx-board-back") | |
| page.locator(".ex-choice").first.click() | |
| page.wait_for_selector(".ex-ok, .ex-bad") | |
| rnd = store.attempts_of(KID_ID, "lesson_round")[0] | |
| wait_until(lambda: key in store.work_of(KID_ID, rnd["id"])) | |
| assert len(store.work_of(KID_ID, rnd["id"])) == 1 # the first answer had no board | |
| assert page.errors == [] | |
| def test_after_a_practice_test_the_student_sees_the_review_of_the_way(browser, bank): | |
| from datetime import timedelta | |
| from mathly2 import boardreview, clock | |
| from mathly2.tests.test_boardreview_api import FakeReviewer | |
| lessons = [l["id"] for l in bank.lessons][:2] | |
| store = InMemoryStore() | |
| klass = store.create_class("setup", "A", lessons=lessons, teacher_email=TEACHER) | |
| store.add_member(klass["id"], KID) | |
| test = store.create_test(klass["id"], clock.school_day() + timedelta(days=9), lessons, TEACHER, created_at=clock.now() - timedelta(days=20)) | |
| app = create_app(Settings(), bank=bank, store=store, reviewer=FakeReviewer()) | |
| with running(app) as url: | |
| page = sign_in(browser, url, f"#/exam/{test['id']}/1/all") | |
| page.wait_for_selector("#qz-q") | |
| page.click("[data-board-toggle]") | |
| page.wait_for_selector(".mx-board.full canvas") | |
| draw_line(page) | |
| page.click(".mx-board .mx-board-back") | |
| page.locator(".qz-choice").first.click() | |
| page.wait_for_selector(".qz-choice.picked") | |
| page.click("#qz-submit") | |
| page.click("#qz-yes") | |
| page.wait_for_selector("#qz-review[data-state='done']", timeout=15000) # pending at first, then the review | |
| text = page.inner_text("#qz-review") | |
| assert "诪砖讜讘 注诇 讛讚专讱" in text and "砖讗诇讛 1" in text and "讞诇拽 诪讛讚专讱 谞讻讜谉" in text and "讘讚拽讜 住讬诪谉" in text | |
| assert page.errors == [] | |
| def test_the_teacher_sees_why_the_ai_failed_and_runs_it_again(browser, bank): | |
| from datetime import timedelta | |
| from mathly2 import clock | |
| from mathly2.tests.test_boardreview_api import FakeReviewer | |
| lessons = [l["id"] for l in bank.lessons][:2] | |
| store = InMemoryStore() | |
| klass = store.create_class("setup", "A", lessons=lessons, teacher_email=TEACHER) | |
| store.add_member(klass["id"], KID) | |
| test = store.create_test(klass["id"], clock.school_day() + timedelta(days=9), lessons, TEACHER, created_at=clock.now() - timedelta(days=20)) | |
| fake = FakeReviewer(fail="rate_limited") | |
| app = create_app(Settings(teachers=frozenset({TEACHER})), bank=bank, store=store, reviewer=fake) | |
| with running(app) as url: | |
| page = sign_in(browser, url, f"#/exam/{test['id']}/1/all") | |
| page.wait_for_selector("#qz-q") | |
| page.click("[data-board-toggle]") | |
| page.wait_for_selector(".mx-board.full canvas") | |
| draw_line(page) | |
| page.click(".mx-board .mx-board-back") | |
| page.locator(".qz-choice").first.click() | |
| page.wait_for_selector(".qz-choice.picked") | |
| page.click("#qz-submit") | |
| page.click("#qz-yes") | |
| page.wait_for_selector("#qz-review[data-state='failed']", timeout=15000) | |
| attempt = store.attempts_of(KID_ID, "exercise_test")[0] | |
| teacher = teacher_page(browser, url) | |
| teacher.click("#teacher-tabs [data-tab='work']") | |
| teacher.click(f"[data-open='{attempt['id']}']") | |
| teacher.wait_for_selector("#work-ai[data-state='failed']") | |
| assert "讛诪讻住讛 讛讞讬谞诪讬转" in teacher.inner_text("#work-ai") | |
| second = attempt["questions"][1]["key"] | |
| teacher.click(f".work-q[data-key='{second}'] [data-points='0.5']") # picked, not saved yet | |
| fake.fail = None | |
| teacher.click("#work-ai-run") | |
| teacher.wait_for_selector("#work-ai[data-state='done']", timeout=15000) | |
| assert "住讬讻讜诐 讛志AI" in teacher.inner_text("#work-ai") and teacher.locator(".work-q .work-ai").count() == 1 | |
| assert teacher.get_attribute(f".work-q[data-key='{second}'] [data-points='0.5']", "aria-pressed") == "true" # the pick stayed | |
| first = attempt["questions"][0]["key"] | |
| asked = len(fake.calls) | |
| teacher.click(f".work-q[data-key='{first}'] [data-ai-key]") # one question alone | |
| teacher.wait_for_selector("#work-ai[data-state='done']", timeout=15000) | |
| assert len(fake.calls) == asked + 1 and fake.calls[-1]["n"] == 1 | |
| assert teacher.errors == [] | |
| def test_while_the_ai_works_the_student_and_the_teacher_see_its_steps(browser, bank): | |
| import threading | |
| from datetime import timedelta | |
| from mathly2 import clock | |
| from mathly2.tests.test_boardreview_api import FakeReviewer | |
| release = threading.Event() | |
| class Slow(FakeReviewer): | |
| def review_one(self, item, on_event=lambda step, **info: None): | |
| on_event("busy", seconds=30) | |
| release.wait(30) | |
| return super().review_one(item, on_event) | |
| lessons = [l["id"] for l in bank.lessons][:2] | |
| store = InMemoryStore() | |
| klass = store.create_class("setup", "A", lessons=lessons, teacher_email=TEACHER) | |
| store.add_member(klass["id"], KID) | |
| test = store.create_test(klass["id"], clock.school_day() + timedelta(days=9), lessons, TEACHER, created_at=clock.now() - timedelta(days=20)) | |
| app = create_app(Settings(teachers=frozenset({TEACHER})), bank=bank, store=store, reviewer=Slow()) | |
| with running(app) as url: | |
| page = sign_in(browser, url, f"#/exam/{test['id']}/1/all") | |
| page.wait_for_selector("#qz-q") | |
| page.click("[data-board-toggle]") | |
| page.wait_for_selector(".mx-board.full canvas") | |
| draw_line(page) | |
| page.click(".mx-board .mx-board-back") | |
| page.locator(".qz-choice").first.click() | |
| page.wait_for_selector(".qz-choice.picked") | |
| page.click("#qz-submit") | |
| page.click("#qz-yes") | |
| page.wait_for_selector("#qz-review[data-state='pending'] .rv-steps li.rv-now:has-text('注诪讜住')", timeout=15000) | |
| assert "谞诪爪讗讜 诇讜讞 讗讞讚" in page.inner_text("#qz-review") and page.locator(".rv-elapsed").count() == 1 | |
| attempt = store.attempts_of(KID_ID, "exercise_test")[0] | |
| teacher = teacher_page(browser, url) | |
| teacher.click("#teacher-tabs [data-tab='work']") | |
| teacher.click(f"[data-open='{attempt['id']}']") | |
| teacher.wait_for_selector("#work-ai[data-state='pending'] .rv-steps") | |
| assert "注诪讜住" in teacher.inner_text("#work-ai") | |
| release.set() | |
| page.wait_for_selector("#qz-review[data-state='done']", timeout=15000) | |
| teacher.wait_for_selector("#work-ai[data-state='done']", timeout=15000) | |
| assert page.errors == [] and teacher.errors == [] | |