wzender commited on
Commit
ca804b9
·
unverified ·
1 Parent(s): e22090e

Tests follow board work; home spec validates again (#11)

Browse files

The tests expect board work's "work" fields and board functions; tests about other things count the board as written on. The home spec's "Nothing critical" scenario is back.

mathly2/tests/serve.py CHANGED
@@ -39,3 +39,20 @@ def only(bank, lessons):
39
  from mathly2.bank import Bank
40
  keep = set(lessons)
41
  return Bank({**bank.data, "units": [u for u in bank.lessons if u["id"] in keep], "skills": [s for s in bank.skills if s["unit"] in keep]})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  from mathly2.bank import Bank
40
  keep = set(lessons)
41
  return Bank({**bank.data, "units": [u for u in bank.lessons if u["id"] in keep], "skills": [s for s in bank.skills if s["unit"] in keep]})
42
+
43
+
44
+ # The board as if the student wrote the way on it (board-work): for tests about something else, a quiz or a test then takes a choice
45
+ # without drawing first. No picture is sent (pictureOf gives nothing). The board-work tests draw on the real board instead.
46
+ _BOARD_WRITTEN = """(() => {
47
+ const patch = b => { b.hasWork = () => true; b.workSig = () => "test"; b.pictureOf = async () => null; return b; };
48
+ if (window.MathlyBoard) { patch(window.MathlyBoard); return; }
49
+ let board;
50
+ Object.defineProperty(window, "MathlyBoard", { configurable: true, get: () => board, set: b => { board = patch(b); } });
51
+ })()"""
52
+
53
+
54
+ def board_written(page):
55
+ """From now on (and after a reload) every board on the page counts as written on."""
56
+ page.add_init_script(_BOARD_WRITTEN)
57
+ page.evaluate(_BOARD_WRITTEN)
58
+ return page
mathly2/tests/test_assignments_api.py CHANGED
@@ -467,7 +467,7 @@ def counts(client, who=TEACHER):
467
 
468
 
469
  def test_the_counts_are_classes_tests_assignments_and_submissions_waiting(world):
470
- assert counts(world) == {"classes": 1, "tests": 0, "assignments": 0, "inbox": 0, "progress": 2, "ai": 0}
471
  for _ in range(3):
472
  world.store.create_test(world.a["id"], clock.school_day() + 10 * DAY, [world.quiz_lesson], TEACHER, created_at=clock.now())
473
  a = give(world, kind="quiz", quiz_lesson=world.quiz_lesson).json()
@@ -478,7 +478,7 @@ def test_the_counts_are_classes_tests_assignments_and_submissions_waiting(world)
478
  submit(world, a["id"], KID)
479
  submit(world, a["id"], KID2)
480
  submit(world, b["id"], KID)
481
- assert counts(world) == {"classes": 1, "tests": 4, "assignments": 4, "inbox": 3, "progress": 2, "ai": 0}
482
 
483
 
484
  def test_checking_a_submission_lowers_the_inbox_and_removed_items_are_not_counted(world):
@@ -490,13 +490,13 @@ def test_checking_a_submission_lowers_the_inbox_and_removed_items_are_not_counte
490
  assert counts(world)["inbox"] == 0
491
  world.store.remove_test(test["id"])
492
  world.delete(f"/api/assignments/{a['id']}", headers=auth(TEACHER))
493
- assert counts(world) == {"classes": 1, "tests": 0, "assignments": 0, "inbox": 0, "progress": 2, "ai": 0}
494
 
495
 
496
  def test_another_teachers_items_are_not_counted_and_a_developer_counts_everything(world):
497
  give(world, kind="quiz", quiz_lesson=world.quiz_lesson)
498
  world.store.create_test(world.b["id"], clock.school_day() + 10 * DAY, [world.other_quiz], OTHER_TEACHER, created_at=clock.now())
499
- assert counts(world, OTHER_TEACHER) == {"classes": 1, "tests": 1, "assignments": 0, "inbox": 0, "progress": 1, "ai": 0}
500
- assert counts(world) == {"classes": 1, "tests": 0, "assignments": 1, "inbox": 0, "progress": 2, "ai": 0}
501
- assert counts(world, DEV) == {"classes": 2, "tests": 1, "assignments": 1, "inbox": 0, "progress": 3, "ai": 0}
502
  assert world.get("/api/teaching/counts", headers=auth(KID)).status_code == 403
 
467
 
468
 
469
  def test_the_counts_are_classes_tests_assignments_and_submissions_waiting(world):
470
+ assert counts(world) == {"classes": 1, "tests": 0, "assignments": 0, "inbox": 0, "progress": 2, "ai": 0, "work": 0}
471
  for _ in range(3):
472
  world.store.create_test(world.a["id"], clock.school_day() + 10 * DAY, [world.quiz_lesson], TEACHER, created_at=clock.now())
473
  a = give(world, kind="quiz", quiz_lesson=world.quiz_lesson).json()
 
478
  submit(world, a["id"], KID)
479
  submit(world, a["id"], KID2)
480
  submit(world, b["id"], KID)
481
+ assert counts(world) == {"classes": 1, "tests": 4, "assignments": 4, "inbox": 3, "progress": 2, "ai": 0, "work": 0}
482
 
483
 
484
  def test_checking_a_submission_lowers_the_inbox_and_removed_items_are_not_counted(world):
 
490
  assert counts(world)["inbox"] == 0
491
  world.store.remove_test(test["id"])
492
  world.delete(f"/api/assignments/{a['id']}", headers=auth(TEACHER))
493
+ assert counts(world) == {"classes": 1, "tests": 0, "assignments": 0, "inbox": 0, "progress": 2, "ai": 0, "work": 0}
494
 
495
 
496
  def test_another_teachers_items_are_not_counted_and_a_developer_counts_everything(world):
497
  give(world, kind="quiz", quiz_lesson=world.quiz_lesson)
498
  world.store.create_test(world.b["id"], clock.school_day() + 10 * DAY, [world.other_quiz], OTHER_TEACHER, created_at=clock.now())
499
+ assert counts(world, OTHER_TEACHER) == {"classes": 1, "tests": 1, "assignments": 0, "inbox": 0, "progress": 1, "ai": 0, "work": 0}
500
+ assert counts(world) == {"classes": 1, "tests": 0, "assignments": 1, "inbox": 0, "progress": 2, "ai": 0, "work": 0}
501
+ assert counts(world, DEV) == {"classes": 2, "tests": 1, "assignments": 1, "inbox": 0, "progress": 3, "ai": 0, "work": 0}
502
  assert world.get("/api/teaching/counts", headers=auth(KID)).status_code == 403
mathly2/tests/test_board_browser.py CHANGED
@@ -79,7 +79,7 @@ def test_the_math_keyboard_file_is_the_unedited_copy_from_mathly1():
79
  def test_the_board_loads_and_opens_in_light_and_dark_with_no_error(browser, scheme):
80
  with running(create_app()) as url:
81
  page = board_page(browser, url, DESKTOP, scheme)
82
- assert page.evaluate("Object.keys(window.MathlyBoard).sort()") == ["button", "closeFull", "desk", "isOn", "mount", "openFull", "show", "toggle"]
83
  page.click("[data-board-toggle]")
84
  page.wait_for_selector(".mx-board")
85
  assert page.is_visible(".mx-board") and page.is_visible(".mx-board-bar")
 
79
  def test_the_board_loads_and_opens_in_light_and_dark_with_no_error(browser, scheme):
80
  with running(create_app()) as url:
81
  page = board_page(browser, url, DESKTOP, scheme)
82
+ assert page.evaluate("Object.keys(window.MathlyBoard).sort()") == ["button", "closeFull", "desk", "hasWork", "isOn", "mount", "openFull", "pictureOf", "show", "toggle", "workSig"]
83
  page.click("[data-board-toggle]")
84
  page.wait_for_selector(".mx-board")
85
  assert page.is_visible(".mx-board") and page.is_visible(".mx-board-bar")
mathly2/tests/test_home_browser.py CHANGED
@@ -13,6 +13,7 @@ from mathly2.config import Settings
13
  from mathly2.main import create_app
14
  from mathly2.store import InMemoryStore
15
  from mathly2.tests.serve import offline_formulas, only, running
 
16
 
17
  KID, TEACHER = "kid@school.test", "t@school.test"
18
  KID_ID = str(uuid.uuid5(uuid.NAMESPACE_DNS, KID))
@@ -475,7 +476,8 @@ def test_a_refused_test_shows_why_and_keeps_what_was_typed(browser, bank):
475
  # --- 5.1 the whole countdown ---
476
 
477
  def answer_in_browser(page, bank, store, kind, scope, rights):
478
- """Answer the open attempt on screen right or wrong, one flag per question, moving with "next"; then submit."""
 
479
  page.wait_for_selector("#qz-q")
480
  refs = store.attempts_of(KID_ID, kind, scope)[-1]["questions"]
481
  for n, ok in enumerate(rights):
 
13
  from mathly2.main import create_app
14
  from mathly2.store import InMemoryStore
15
  from mathly2.tests.serve import offline_formulas, only, running
16
+ from mathly2.tests.serve import board_written
17
 
18
  KID, TEACHER = "kid@school.test", "t@school.test"
19
  KID_ID = str(uuid.uuid5(uuid.NAMESPACE_DNS, KID))
 
476
  # --- 5.1 the whole countdown ---
477
 
478
  def answer_in_browser(page, bank, store, kind, scope, rights):
479
+ """Answer the open attempt on screen right or wrong, one flag per question, moving with "next"; then submit. The board counts as written on."""
480
+ board_written(page)
481
  page.wait_for_selector("#qz-q")
482
  refs = store.attempts_of(KID_ID, kind, scope)[-1]["questions"]
483
  for n, ok in enumerate(rights):
mathly2/tests/test_quiz_browser.py CHANGED
@@ -13,7 +13,7 @@ from mathly2.bank import load_bank
13
  from mathly2.config import Settings
14
  from mathly2.main import create_app
15
  from mathly2.store import InMemoryStore
16
- from mathly2.tests.serve import offline_formulas, only, running
17
 
18
  KID = "kid@school.test"
19
  KID_ID = str(uuid.uuid5(uuid.NAMESPACE_DNS, KID))
@@ -61,7 +61,7 @@ def seed_attempt(store, bank, lesson, grade=None, minutes_left=None, answers=Non
61
 
62
 
63
  def path_page(browser, url, where="", viewport=PHONE):
64
- page = offline_formulas(browser.new_page(viewport=viewport))
65
  errors = []
66
  page.on("pageerror", lambda e: errors.append(str(e)))
67
  page.goto(url + where)
 
13
  from mathly2.config import Settings
14
  from mathly2.main import create_app
15
  from mathly2.store import InMemoryStore
16
+ from mathly2.tests.serve import board_written, offline_formulas, only, running
17
 
18
  KID = "kid@school.test"
19
  KID_ID = str(uuid.uuid5(uuid.NAMESPACE_DNS, KID))
 
61
 
62
 
63
  def path_page(browser, url, where="", viewport=PHONE):
64
+ page = board_written(offline_formulas(browser.new_page(viewport=viewport))) # these tests are not about the work on the board
65
  errors = []
66
  page.on("pageerror", lambda e: errors.append(str(e)))
67
  page.goto(url + where)
mathly2/tests/test_written_browser.py CHANGED
@@ -837,11 +837,11 @@ def test_every_teacher_tab_shows_its_number_and_the_numbers_change_after_grading
837
  page.wait_for_selector("#teaching")
838
  page.wait_for_function("document.querySelector('[data-count=inbox]')?.innerText !== ''")
839
  numbers = lambda: {el.get_attribute("data-count"): el.inner_text() for el in page.locator("#teacher-tabs [data-count]").all()}
840
- assert numbers() == {"classes": "1", "tests": "3", "assignments": "2", "inbox": "2", "progress": "2", "ai": "0"}
841
  for tab in ("tests", "assignments", "inbox"): # on every tab
842
  page.click(f"#teacher-tabs [data-tab='{tab}']")
843
  page.wait_for_function("(t) => document.querySelector(`#teacher-tabs [data-tab='${t}'][aria-pressed='true']`) && [...document.querySelectorAll('#teacher-tabs [data-count]')].every(e => e.innerText !== '')", arg=tab)
844
- assert numbers() == {"classes": "1", "tests": "3", "assignments": "2", "inbox": "2", "progress": "2", "ai": "0"}, tab
845
  page.click(f"[data-open='{monday['id']}']")
846
  page.fill("[name=grade]", "80")
847
  page.click("#give-grade")
 
837
  page.wait_for_selector("#teaching")
838
  page.wait_for_function("document.querySelector('[data-count=inbox]')?.innerText !== ''")
839
  numbers = lambda: {el.get_attribute("data-count"): el.inner_text() for el in page.locator("#teacher-tabs [data-count]").all()}
840
+ assert numbers() == {"classes": "1", "tests": "3", "assignments": "2", "inbox": "2", "progress": "2", "ai": "0", "work": "0"}
841
  for tab in ("tests", "assignments", "inbox"): # on every tab
842
  page.click(f"#teacher-tabs [data-tab='{tab}']")
843
  page.wait_for_function("(t) => document.querySelector(`#teacher-tabs [data-tab='${t}'][aria-pressed='true']`) && [...document.querySelectorAll('#teacher-tabs [data-count]')].every(e => e.innerText !== '')", arg=tab)
844
+ assert numbers() == {"classes": "1", "tests": "3", "assignments": "2", "inbox": "2", "progress": "2", "ai": "0", "work": "0"}, tab
845
  page.click(f"[data-open='{monday['id']}']")
846
  page.fill("[name=grade]", "80")
847
  page.click("#give-grade")
mathly2/tests/test_written_store.py CHANGED
@@ -45,9 +45,9 @@ def test_picture_refs_find_assignments_submissions_and_requests(store):
45
  s = store.save_submission(a["id"], U, pictures=[lone["id"]])
46
  r = store.create_photo_request(U, a["id"], "d1", "1234", NOW, NOW + timedelta(minutes=10))
47
  store.save_photo_request(r["id"], state="received", picture_id=lone["id"])
48
- assert store.picture_refs(pic["id"]) == {"assignments": [a["id"]], "submissions": [], "requests": [], "ai_requests": []}
49
- assert store.picture_refs(lone["id"]) == {"assignments": [], "submissions": [s["id"]], "requests": [r["id"]], "ai_requests": []}
50
- assert store.picture_refs(picture(store)["id"]) == {"assignments": [], "submissions": [], "requests": [], "ai_requests": []}
51
 
52
 
53
  # --- 1.1 assignments ---
@@ -172,8 +172,9 @@ def test_supabase_requests_for_pictures_and_assignments():
172
  assert s.calls[4][3]["due_date"] == "2026-11-20" and s.calls[4][3]["pages"] == ["p1"] and made_a["due_date"] == DUE
173
  assert s.get_assignment(3)["pages"] == ["p1"] and [a["id"] for a in s.assignments_of_classes([2, 1])] == [3]
174
  assert s.calls[6][2] == {"class_id": "in.(1,2)", "select": "*", "order": "due_date,id", "removed_at": "is.null"}
175
- s2 = Recorder([[{"id": 3}], [{"id": 9}], [{"id": 4}], [{"id": "a1"}]])
176
- assert s2.picture_refs("p1") == {"assignments": [3], "submissions": [9], "requests": [4], "ai_requests": ["a1"]}
 
177
  assert s2.calls[0][2] == {"pages": "cs.{p1}", "select": "id"} and s2.calls[1][2] == {"pictures": "cs.{p1}", "select": "id"}
178
 
179
 
 
45
  s = store.save_submission(a["id"], U, pictures=[lone["id"]])
46
  r = store.create_photo_request(U, a["id"], "d1", "1234", NOW, NOW + timedelta(minutes=10))
47
  store.save_photo_request(r["id"], state="received", picture_id=lone["id"])
48
+ assert store.picture_refs(pic["id"]) == {"assignments": [a["id"]], "submissions": [], "requests": [], "ai_requests": [], "work": []}
49
+ assert store.picture_refs(lone["id"]) == {"assignments": [], "submissions": [s["id"]], "requests": [r["id"]], "ai_requests": [], "work": []}
50
+ assert store.picture_refs(picture(store)["id"]) == {"assignments": [], "submissions": [], "requests": [], "ai_requests": [], "work": []}
51
 
52
 
53
  # --- 1.1 assignments ---
 
172
  assert s.calls[4][3]["due_date"] == "2026-11-20" and s.calls[4][3]["pages"] == ["p1"] and made_a["due_date"] == DUE
173
  assert s.get_assignment(3)["pages"] == ["p1"] and [a["id"] for a in s.assignments_of_classes([2, 1])] == [3]
174
  assert s.calls[6][2] == {"class_id": "in.(1,2)", "select": "*", "order": "due_date,id", "removed_at": "is.null"}
175
+ s2 = Recorder([[{"id": 3}], [{"id": 9}], [{"id": 4}], [{"id": "a1"}], [{"ref": "12", "question_key": "q1"}]])
176
+ assert s2.picture_refs("p1") == {"assignments": [3], "submissions": [9], "requests": [4], "ai_requests": ["a1"], "work": ["12:q1"]}
177
+ assert s2.calls[4][1] == "m2_work" and s2.calls[4][2] == {"picture_id": "eq.p1", "select": "ref,question_key"}
178
  assert s2.calls[0][2] == {"pages": "cs.{p1}", "select": "id"} and s2.calls[1][2] == {"pictures": "cs.{p1}", "select": "id"}
179
 
180
 
openspec/changes/game-home-lesson-rounds/specs/student-home/spec.md CHANGED
@@ -47,6 +47,10 @@ The path SHALL be reached from its tab, "העמקה"; the home has no other link
47
  ### Requirement: Home shows only what is critical
48
  The home SHALL show only: the stars and goal, the play button, the lessons board, the nearest test's strip, a required simulation, the number of new assignments (as one button to "הגשות") and photo help. Other tests, the readiness bar, the practice tests, assignments that were handed in or checked, and settings SHALL NOT be on home; they are on the test's card and in their tabs.
49
 
 
 
 
 
50
  #### Scenario: Two tests
51
  - **WHEN** a student has two upcoming tests
52
  - **THEN** home shows the nearest one, and the other is listed on that test's card
 
47
  ### Requirement: Home shows only what is critical
48
  The home SHALL show only: the stars and goal, the play button, the lessons board, the nearest test's strip, a required simulation, the number of new assignments (as one button to "הגשות") and photo help. Other tests, the readiness bar, the practice tests, assignments that were handed in or checked, and settings SHALL NOT be on home; they are on the test's card and in their tabs.
49
 
50
+ #### Scenario: Nothing critical
51
+ - **WHEN** a student has no upcoming test, no new assignment and no lesson behind
52
+ - **THEN** home shows the stars and goal, the play button and the lessons, and no test strip or buttons under it
53
+
54
  #### Scenario: Two tests
55
  - **WHEN** a student has two upcoming tests
56
  - **THEN** home shows the nearest one, and the other is listed on that test's card