Download mathly2/tests/test_ai_browser.py from wzender/mathly: direct link, hf CLI and curl.
- Browser
- Download file 10.5 kB
-
https://huggingface.co/spaces/wzender/mathly/resolve/main/mathly2/tests/test_ai_browser.py
- Command line
-
hf download hf://spaces/wzender/mathly/mathly2/tests/test_ai_browser.py
-
curl -L -o test_ai_browser.py https://huggingface.co/spaces/wzender/mathly/resolve/main/mathly2/tests/test_ai_browser.py
10.5 kB
| """AI help in a real browser (add-ai-help 3.1, 3.2): the student's photo help and the teacher's queue.""" | |
| import json | |
| import urllib.request | |
| import pytest | |
| playwright = pytest.importorskip("playwright.sync_api") | |
| from mathly2 import ai, clock | |
| 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_written_browser import KID, PHONE, TEACHER, bank, browser, day, jpeg, uid # noqa: F401 | |
| def make(bank, ai_on=True, fake=None, daily=5): | |
| store = InMemoryStore() | |
| lesson = next(u["id"] for u in bank.data["units"] if u.get("quiz")) | |
| klass = store.create_class("setup", "A", lessons=[lesson], teacher_email=TEACHER, ai_enabled=ai_on, ai_daily_checks=daily) | |
| store.add_member(klass["id"], KID) | |
| store.link_user(KID, uid(KID)) | |
| fake = fake or ai.FakeAI() | |
| return create_app(Settings(), bank=bank, store=store, ai_client=fake), store, klass, fake, lesson | |
| def call(url, path, who, method="POST", body=None): | |
| req = urllib.request.Request(url + path, method=method, data=json.dumps(body or {}).encode() if method != "GET" else None, | |
| headers={"Authorization": f"Bearer dev:{who}", "Content-Type": "application/json"}) | |
| with urllib.request.urlopen(req) as r: | |
| return json.loads(r.read() or b"null") | |
| def student(browser, url, where="", email=KID): | |
| page = offline_formulas(browser.new_page(viewport=PHONE)) | |
| errors = [] | |
| page.on("pageerror", lambda e: errors.append(str(e))) | |
| page.goto(url + where) | |
| page.fill("#email", email) | |
| page.click("#dev button") | |
| page.errors = errors | |
| return page | |
| def to_home(page): | |
| page.click(".subject-card") | |
| page.wait_for_selector(".tabs") | |
| def teacher(browser, url, tab="ai"): | |
| page = offline_formulas(browser.new_page(viewport=PHONE)) | |
| page.goto(url) | |
| page.fill("#email", TEACHER) | |
| page.click("#dev button") | |
| page.wait_for_selector("#teaching") | |
| if tab: | |
| page.click(f"#teacher-tabs [data-tab={tab}]") | |
| return page | |
| def send_photo(page): | |
| page.click("#ai-new") | |
| page.wait_for_selector(".ai-window") | |
| page.set_input_files("[data-ai-file] >> nth=-1", files=[{"name": "p.jpg", "mimeType": "image/jpeg", "buffer": jpeg()}]) | |
| page.click("#ai-send") | |
| page.wait_for_selector("#ai-state") | |
| def test_no_photo_help_button_or_card_when_the_class_has_the_ai_off(browser, bank): | |
| app, store, klass, fake, lesson = make(bank, ai_on=False) | |
| with running(app) as url: | |
| page = student(browser, url) | |
| to_home(page) | |
| page.wait_for_selector("#lesson-map") | |
| assert page.locator("#ai-card, #ai-new").count() == 0 | |
| skill = next(s["id"] for s in bank.skills if s["unit"] == lesson) | |
| page.goto(url + f"#/skill/{skill}") | |
| page.wait_for_selector(".mx-sheet") | |
| assert page.locator("[data-ai]").count() == 0 | |
| def test_the_student_photographs_waits_then_gets_the_hints_one_at_a_time(browser, bank): | |
| app, store, klass, fake, lesson = make(bank) | |
| with running(app) as url: | |
| page = student(browser, url) | |
| to_home(page) | |
| page.wait_for_selector("#ai-card") | |
| send_photo(page) | |
| assert "诪诪转讬谉 诇诪讜专讛" in page.inner_text("#ai-state") and "讛诪讜专讛 专讜讗讛 讗转 讛转诪讜谞讛" in page.inner_text(".ai-window") and fake.calls == [] | |
| rid = store.ai_requests()[0]["id"] | |
| call(url, f"/api/ai/requests/{rid}/send", TEACHER) | |
| page.wait_for_selector("#ai-steps", timeout=15000) # the page looks again every few seconds | |
| assert page.locator("#ai-steps li").count() == 1 and page.locator("#ai-answer").count() == 0 and page.locator(".ai-work").count() == 0 | |
| page.click("#ai-next-hint") | |
| assert page.locator("#ai-steps li").count() == 2 and page.locator("#ai-next-hint").count() == 0 | |
| page.click("[data-more=work] >> nth=0") | |
| assert page.locator(".ai-work").count() == 1 | |
| page.click("#ai-show-answer") | |
| assert "x = 2" in page.inner_text("#ai-answer") or "2" in page.inner_text("#ai-answer") | |
| assert not page.errors | |
| def test_a_waiting_request_can_be_cancelled_and_a_decline_shows_the_note(browser, bank): | |
| app, store, klass, fake, lesson = make(bank) | |
| with running(app) as url: | |
| page = student(browser, url) | |
| to_home(page) | |
| page.wait_for_selector("#ai-card") | |
| send_photo(page) | |
| page.click("#ai-cancel") | |
| page.wait_for_function("document.querySelector('#ai-state').innerText.includes('讘讜讟诇')") | |
| page.click("[data-ai-close]") | |
| send_photo(page) | |
| rid = [r for r in store.ai_requests() if r["state"] == "waiting"][0]["id"] | |
| call(url, f"/api/ai/requests/{rid}/decline", TEACHER, body={"note": "爪诇诪讜 砖讜讘"}) | |
| page.wait_for_selector("#ai-note", timeout=15000) | |
| assert "爪诇诪讜 砖讜讘" in page.inner_text("#ai-note") and page.locator("#ai-photo").count() == 0 | |
| def test_the_exercise_screens_camera_button_tells_the_teacher_which_skill_and_the_queue_shows_it(browser, bank): | |
| app, store, klass, fake, lesson = make(bank) | |
| skill = next(s for s in bank.skills if s["unit"] == lesson) | |
| with running(app) as url: | |
| page = student(browser, url, f"#/skill/{skill['id']}") | |
| page.wait_for_selector("[data-ai]") | |
| page.click(".ex-ai") | |
| page.wait_for_selector(".ai-window") | |
| page.set_input_files("[data-ai-file] >> nth=-1", files=[{"name": "p.jpg", "mimeType": "image/jpeg", "buffer": jpeg()}]) | |
| page.click("#ai-send") | |
| page.wait_for_selector("#ai-state") | |
| t = teacher(browser, url) | |
| t.wait_for_selector("#ai-waiting") | |
| assert skill["name"].replace("$", "")[:6] in t.inner_text(".ai-item").replace("$", "") or t.locator(".ai-item").count() == 1 | |
| assert t.inner_text("#teacher-tabs [data-tab=ai] .tab-count") == "1" and t.locator(".ai-item img").count() == 1 | |
| assert store.ai_requests()[0]["skill_id"] == skill["id"] | |
| def test_the_teacher_sends_and_it_shows_done_a_failure_shows_its_cause_and_a_retry_works(browser, bank): | |
| fake = ai.FakeAI(fail="no_credit") | |
| app, store, klass, _, lesson = make(bank, fake=fake) | |
| with running(app) as url: | |
| pic = __import__("mathly2.pictures", fromlist=["x"]).store_picture(store, app.state.pictures, uid(KID), "help", jpeg()) | |
| store.create_ai_request("photo", uid(KID), klass["id"], picture_id=pic["id"], created_at=clock.now()) | |
| t = teacher(browser, url) | |
| t.wait_for_selector("#ai-waiting .ai-item") | |
| t.click("[data-send]") | |
| t.wait_for_selector("#ai-rest [data-state=failed]", timeout=15000) | |
| text = t.inner_text("#ai-rest") | |
| assert "谞讙诪专讛 讛讬转专讛" in text and "诇讛讜住讬祝 讬转专讛" in text and "fake" in text # the cause, what to do, the detail | |
| fake.fail = None | |
| t.click("#ai-rest [data-send]") | |
| t.wait_for_selector("#ai-rest [data-state=done]", timeout=15000) | |
| assert t.locator("#ai-waiting").count() == 0 and t.inner_text("#teacher-tabs [data-tab=ai] .tab-count") == "0" | |
| def test_a_batch_with_a_short_budget_says_so_and_a_decline_has_a_note_box(browser, bank): | |
| app, store, klass, fake, lesson = make(bank, daily=2) | |
| with running(app) as url: | |
| for _ in range(4): | |
| pic = __import__("mathly2.pictures", fromlist=["x"]).store_picture(store, app.state.pictures, uid(KID), "help", jpeg()) | |
| store.create_ai_request("photo", uid(KID), klass["id"], picture_id=pic["id"], created_at=clock.now()) | |
| t = teacher(browser, url) | |
| t.wait_for_selector("#ai-batches") | |
| assert "谞砖讗专讜 2 诪转讜讱 2" in t.inner_text("#ai-budgets") | |
| t.click("[data-batch=photos]") | |
| t.wait_for_selector("#ai-message:not([hidden])") | |
| assert "谞砖诇讞讜 2" in t.inner_text("#ai-message") and "诪讞专" in t.inner_text("#ai-message") | |
| assert t.locator("#ai-waiting .ai-item").count() == 2 | |
| t.click("[data-decline-open] >> nth=0") | |
| assert t.locator("[data-decline]:not([hidden]) [data-note]").count() == 1 | |
| def test_a_check_is_asked_by_the_student_shown_to_the_teacher_as_a_suggestion_and_the_grade_is_the_teachers(browser, bank): | |
| app, store, klass, fake, lesson = make(bank) | |
| row = store.create_assignment(klass["id"], "讘讜讞谉 讻转讜讘", day(5), TEACHER, "quiz", quiz_lesson=lesson, created_at=clock.now()) | |
| from mathly2 import pictures | |
| pic = pictures.store_picture(store, app.state.pictures, uid(KID), "answer", jpeg("blue")) | |
| store.save_submission(row["id"], uid(KID), pictures=[pic["id"]], state="handed_in", late=False, handed_at=clock.now(), grade=None, teacher_note="") | |
| with running(app) as url: | |
| page = student(browser, url, f"#/assignment/{row['id']}") | |
| page.wait_for_selector("#ask-check") | |
| page.click("#ask-check") | |
| page.wait_for_selector("#ai-box [data-state=waiting]") | |
| rid = store.ai_requests(kinds=["check"])[0]["id"] | |
| t = teacher(browser, url, tab="inbox") | |
| assert t.locator("#teacher-tabs [data-tab=ai] .tab-count").inner_text() in ("1", "") | |
| call(url, f"/api/ai/requests/{rid}/send", TEACHER) | |
| page.wait_for_selector("#ai-box [data-state=done]", timeout=15000) | |
| assert "讛爪讬讜谉 讛诪讜爪注" in page.inner_text("#ai-box") and "讛诪讜专讛 拽讜讘注" in page.inner_text("#ai-box") | |
| t.reload() # still signed in | |
| t.wait_for_selector("#teaching") | |
| t.click("#teacher-tabs [data-tab=inbox]") | |
| t.click("[data-open]") | |
| t.wait_for_selector("#ai-suggestion") | |
| assert t.inner_text("#ai-score") == "100" and "讛爪讬讜谉 砖诇讻诐 拽讜讘注" in t.inner_text("#ai-suggestion") | |
| t.fill("[name=grade]", "70") | |
| t.click("#give-grade") | |
| t.wait_for_selector("#inbox-notice") | |
| assert store.get_submission(row["id"], uid(KID))["grade"] == 70 and store.get_ai_request(rid)["result"]["score"] == 100 | |
| page.reload() | |
| page.goto(url + f"#/assignment/{row['id']}") | |
| page.wait_for_selector("#ask-again") | |
| page.click("#ask-again") | |
| page.wait_for_selector("#again-asked") | |
| t.click("#teacher-tabs [data-tab=inbox]") | |
| t.wait_for_selector("[data-kind=again]") | |
| assert "讘讬拽砖/讛 讘讚讬拽讛 讞讜讝专转" in t.inner_text("#inbox-list") | |