File size: 10,496 Bytes
fd475e7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cff6a0c
fd475e7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
"""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")