File size: 8,368 Bytes
72ba2ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e22090e
 
 
82cae90
72ba2ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cff6a0c
 
 
 
 
 
 
 
 
 
 
 
 
 
82cae90
cff6a0c
 
 
 
e22090e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""The home's lessons map and a lesson round in a real browser (game-home-lesson-rounds 2.1-2.3)."""
import os
from datetime import timedelta

import pytest

playwright = pytest.importorskip("playwright.sync_api")

from mathly2 import clock
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, only, running
from mathly2.tests.test_home_browser import KID, KID_ID, PHONE, TEACHER, no_sideways_scroll, sign_in, to_home

SHOTS = os.environ.get("MATHLY_SHOTS")          # a folder: save screenshots there to look at the screens
STAMP = "2026-10-05T10:00:00+00:00"


@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 world(bank, n=4):
    lessons = [l["id"] for l in bank.lessons if any(s["unit"] == l["id"] and bank.question_keys(s["id"]) for s in bank.skills)][:n]
    store = InMemoryStore()
    klass = store.create_class("setup", "A", lessons=lessons, teacher_email=TEACHER)
    store.add_member(klass["id"], KID)
    small = only(bank, lessons)
    return create_app(Settings(), bank=small, store=store), store, small, lessons


def shot(page, name):
    if SHOTS:
        page.screenshot(path=os.path.join(SHOTS, f"{name}.png"), full_page=True)


def right_index(bank, key):
    skill = next(s for s in bank.skills if key in bank.question_keys(s["id"]))
    from mathly2.bank import question_key
    question = next(q for q in skill["questions"] if question_key(skill["id"], q) == key)
    return next(i for i, c in enumerate(question["choices"]) if c.get("correct"))


def test_home_is_a_map_of_the_lessons_with_stars_and_a_play_button(browser, bank):
    app, store, small, lessons = world(bank)
    first = [s["id"] for s in small.skills if s["unit"] == lessons[0]]
    for s in first:
        store.save_progress(KID_ID, s, started_at=STAMP, mastered_at=STAMP)                    # lesson 1 perfect
    store.set_lesson_dates({lessons[2]: clock.school_day() - timedelta(days=3)})               # lesson 3 behind
    with running(app) as url:
        page = sign_in(browser, url)
        to_home(page)
        page.wait_for_selector("#lesson-map")
        nodes = page.locator(".lnode")
        assert nodes.count() == len(lessons)
        first, late = page.locator(f".lnode[data-lesson='{lessons[0]}']"), page.locator(f".lnode[data-lesson='{lessons[2]}']")
        assert first.get_attribute("data-stars") == "3" and "👑" in first.text_content()
        assert "⏰" in late.text_content()
        assert page.locator("#stars-count").inner_text() == f"⭐ 3/{3 * len(lessons)}"
        assert page.locator("#play").get_attribute("href") == f"#/round/{lessons[2]}"            # the behind lesson comes first
        assert page.locator(".lnode.next").get_attribute("data-lesson") == lessons[2]
        assert page.locator(".tabs a").nth(1).inner_text() == "העמקה" and page.locator("#behind-card").count() == 0
        no_sideways_scroll(page)
        shot(page, "home")
        assert page.errors == []


def test_a_round_from_the_map_gives_feedback_and_ends_with_stars(browser, bank):
    app, store, small, lessons = world(bank, 2)
    with running(app) as url:
        page = sign_in(browser, url)
        to_home(page)
        page.click("#play")
        page.wait_for_selector(".round-layer #ex-q")
        total = int(page.locator("#rd-count").inner_text().split()[-1])
        shot(page, "round-question")
        for i in range(total):
            key = page.locator("#ex-q").get_attribute("data-key")
            pick = right_index(small, key) if i < total - 1 else None
            if pick is None:                                                   # the last one wrong
                page.locator(f".ex-choice:not([data-i='{right_index(small, key)}'])").first.click()
                page.wait_for_selector(".ex-bad")
                shot(page, "round-wrong")
            else:
                page.click(f".ex-choice[data-i='{pick}']")
                page.wait_for_selector(".ex-ok")
            page.click("#rd-next")
            if i < total - 1:
                page.wait_for_function(f"document.querySelector('#rd-count').textContent.includes('{i + 2} מתוך')")
        page.wait_for_selector("#rd-result")
        assert page.locator("#rd-score").inner_text().replace("\n", " ").startswith(f"{total - 1}")
        assert page.locator(".rd-bigstars span.on").count() == 2                     # 9 of 10: two stars
        shot(page, "round-result")
        page.click("#rd-result [data-close]")
        page.wait_for_selector(f".lnode[data-lesson='{lessons[0]}'][data-stars='2']")                # the map refreshes with the new stars
        assert page.evaluate("location.hash") == "#/subject/math"
        assert page.errors == []


def test_a_link_to_a_round_opens_the_home_under_it(browser, bank):
    app, _, _, lessons = world(bank, 2)
    with running(app) as url:
        page = sign_in(browser, url, f"#/round/{lessons[1]}")
        page.wait_for_selector(".round-layer #ex-q")
        assert page.locator("#lesson-map").count() == 1
        page.click(".mx-x")
        page.wait_for_function("!document.querySelector('.round-layer')")
        assert page.errors == []


def test_home_is_one_screen_with_every_lesson_a_test_and_an_assignment(browser, bank):
    lessons = [l["id"] for l in bank.lessons]
    store = InMemoryStore()
    klass = store.create_class("setup", "A", lessons=lessons, teacher_email=TEACHER)
    store.add_member(klass["id"], KID)
    store.create_test(klass["id"], clock.school_day() + timedelta(days=9), lessons[:2], TEACHER, created_at=clock.now() - timedelta(days=20))
    store.create_assignment(klass["id"], "דף", clock.school_day() + timedelta(days=3), TEACHER, "pages", pages=["1"])
    with running(create_app(Settings(), bank=bank, store=store)) as url:
        page = sign_in(browser, url)
        to_home(page)
        page.wait_for_selector("#lesson-map")
        page.wait_for_selector("#assignments")
        assert page.locator(".lnode").count() == len(lessons) and page.locator("#nearest-test #home-sim").count() == 1
        assert page.evaluate("document.documentElement.scrollHeight <= window.innerHeight")                    # nothing to scroll
        no_sideways_scroll(page)
        shot(page, "home-one-screen")
        assert page.errors == []


def test_home_shows_the_current_lesson_and_folds_the_past_and_the_coming_ones(browser, bank):
    app, store, small, lessons = world(bank, 5)
    for s in (s["id"] for s in small.skills if s["unit"] == lessons[0]):
        store.save_progress(KID_ID, s, started_at=STAMP, mastered_at=STAMP)                    # lesson 1 done
    today = clock.school_day()
    store.set_lesson_dates({lessons[0]: today - timedelta(days=14), lessons[1]: today - timedelta(days=7),     # 2 is behind
                            lessons[2]: today + timedelta(days=2), lessons[3]: today + timedelta(days=9)})   # 5 has no date
    with running(app) as url:
        page = sign_in(browser, url)
        to_home(page)
        page.wait_for_selector("#lesson-now")
        assert page.locator("#lesson-now .lnode").get_attribute("data-lesson") == lessons[2]
        assert "השיעור הנוכחי" in page.inner_text("#lesson-now")
        past = page.locator("#lessons-past")
        assert [n.get_attribute("data-lesson") for n in past.locator(".lnode").all()] == lessons[:2]
        assert "שיעורים שעברו (2)" in past.locator("summary").inner_text() and "⏰ 1 בפיגור" in past.locator("summary").inner_text()
        assert not past.locator(".lnode").first.is_visible()                                    # folded until opened
        past.locator("summary").click()
        assert past.locator(".lnode").first.is_visible()
        later = page.locator("#lessons-later")
        assert [n.get_attribute("data-lesson") for n in later.locator(".lnode").all()] == lessons[3:5]
        assert page.locator("#play").get_attribute("href") == f"#/round/{lessons[1]}"            # the behind lesson is still played first
        no_sideways_scroll(page)
        shot(page, "home-by-date")
        assert page.errors == []