mathly / mathly2 /tests /test_ai_browser.py
wzender's picture
Refactor home screen layout to a single screen view with lesson tiles, nearest test strip, and new assignments button
cff6a0c
Raw History Blame Contribute Delete
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")