File size: 11,813 Bytes
aeda545
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4a4cb0f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4a13aaf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2067656
 
 
 
 
4a13aaf
aada0ab
 
 
 
 
 
 
 
 
 
2067656
aada0ab
 
2067656
aada0ab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
"""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))


@pytest.fixture(scope="module")
def bank():
    return load_bank()


@pytest.fixture
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 == []