Download tests/test_board_browser.py from wzender/mathly: direct link, hf CLI and curl.
- Browser
- Download file 35.4 kB
-
https://huggingface.co/spaces/wzender/mathly/resolve/main/tests/test_board_browser.py
- Command line
-
hf download hf://spaces/wzender/mathly/tests/test_board_browser.py
-
curl -L -o test_board_browser.py https://huggingface.co/spaces/wzender/mathly/resolve/main/tests/test_board_browser.py
35.4 kB
| """Browser checks of the sketch board beside a skill's exercise (static/math-board.js). | |
| Optional, like the other browser tests: pip install playwright && playwright install chromium.""" | |
| import re | |
| import socket | |
| import threading | |
| import time | |
| import pytest | |
| import uvicorn | |
| from app.main import create_app | |
| playwright = pytest.importorskip("playwright.sync_api") | |
| def server(): | |
| app = create_app() | |
| with socket.socket() as sock: | |
| sock.bind(("127.0.0.1", 0)) | |
| srv = uvicorn.Server(uvicorn.Config(app, log_level="error")) | |
| thread = threading.Thread(target=srv.run, kwargs={"sockets": [sock]}, daemon=True) | |
| thread.start() | |
| try: | |
| deadline = time.monotonic() + 5 | |
| while not srv.started: | |
| assert thread.is_alive() and time.monotonic() < deadline | |
| time.sleep(.01) | |
| yield f"http://127.0.0.1:{sock.getsockname()[1]}" | |
| finally: | |
| srv.should_exit = True | |
| thread.join(timeout=5) | |
| def browser(): | |
| with playwright.sync_playwright() as p: | |
| b = p.chromium.launch() | |
| yield b | |
| b.close() | |
| def open_skill(browser, server, width, height): | |
| ctx = browser.new_context(viewport={"width": width, "height": height}) | |
| ctx.route("https://cdn.jsdelivr.net/**", lambda route: route.abort()) | |
| ctx.route("https://fonts.googleapis.com/**", lambda route: route.abort()) | |
| ctx.add_init_script("try{localStorage.setItem('mathly.sound','off')}catch{}") | |
| page = ctx.new_page() | |
| errors = [] | |
| page.on("pageerror", lambda e: errors.append(str(e))) | |
| page.goto(server) | |
| page.locator('.mx-nav [data-view="path"]').click() # the app opens on home; the trail is on the path tab | |
| playwright.expect(page.locator(".mx-trail")).to_be_visible() | |
| page.locator(".mx-star.st-next").click() | |
| playwright.expect(page.locator(".mx-sheet")).to_be_visible() | |
| return page, errors | |
| def ink_pixels(page): | |
| return page.evaluate("""()=>{ const c=document.querySelector('.mx-board canvas'), d=c.getContext('2d').getImageData(0,0,c.width,c.height).data; | |
| let n=0; for(let i=3;i<d.length;i+=4) if(d[i]) n++; return n; }""") | |
| def draw_line(page): | |
| box = page.locator(".mx-board canvas").bounding_box() | |
| x, y = box["x"] + 60, box["y"] + 60 | |
| page.mouse.move(x, y) | |
| page.mouse.down() | |
| for k in range(1, 12): | |
| page.mouse.move(x + k * 15, y + k * 6) | |
| page.mouse.up() | |
| def touch(page, events): | |
| """Sends pointer events of a finger or a pen to the board: [(type, id, x, y, kind)], x and y from its corner.""" | |
| page.evaluate("""(events)=>{ const c=document.querySelector('.mx-board canvas'), r=c.getBoundingClientRect(); | |
| for(const [type,id,x,y,kind] of events) c.dispatchEvent(new PointerEvent(type,{pointerId:id,pointerType:kind||'touch', | |
| clientX:r.left+x,clientY:r.top+y,button:type==='pointermove'?-1:0,buttons:type==='pointerup'?0:1,pressure:.5,bubbles:true,isPrimary:id===1})); }""", events) | |
| def test_on_a_phone_the_board_opens_full_screen_over_the_exercise(browser, server): | |
| page, errors = open_skill(browser, server, 390, 844) | |
| assert page.locator(".mx-board").count() == 0 # nothing until asked | |
| page.locator("[data-board-toggle]").click() | |
| board = page.locator(".mx-board.full") | |
| playwright.expect(board).to_be_visible() | |
| box = board.bounding_box() | |
| assert box["width"] == 390 and box["height"] == 844 | |
| touch(page, [("pointerdown", 1, 60, 200), *[("pointermove", 1, 60 + k * 20, 200) for k in range(1, 10)], ("pointerup", 1, 240, 200)]) | |
| assert alpha_near(page, 150, 200) > 0 # one finger draws | |
| # two fingers scroll the paper and draw nothing | |
| touch(page, [("pointerdown", 2, 100, 500), ("pointerdown", 3, 200, 500), | |
| *[e for k in range(1, 11) for e in (("pointermove", 2, 100, 500 - k * 30), ("pointermove", 3, 200, 500 - k * 30))], | |
| ("pointerup", 2, 100, 200), ("pointerup", 3, 200, 200)]) | |
| assert alpha_near(page, 150, 200) == 0 and alpha_near(page, 150, 0) == 0 # the line went up out of sight | |
| playwright.expect(page.locator(".mx-board-top")).to_be_visible() | |
| page.locator(".mx-board-top").click() | |
| assert alpha_near(page, 150, 200) > 0 and alpha_near(page, 150, 500) == 0 | |
| # once a pen has written, a resting hand does not draw | |
| touch(page, [("pointerdown", 5, 60, 600, "pen"), ("pointermove", 5, 200, 600, "pen"), ("pointerup", 5, 200, 600, "pen")]) | |
| touch(page, [("pointerdown", 6, 60, 700), ("pointermove", 6, 200, 700), ("pointerup", 6, 200, 700)]) | |
| assert alpha_near(page, 130, 600) > 0 and alpha_near(page, 130, 700) == 0 | |
| # a palm (two contacts) lands while the pen writes: the stroke stays and the paper does not scroll | |
| touch(page, [("pointerdown", 7, 60, 400, "pen"), ("pointermove", 7, 120, 400, "pen"), | |
| ("pointerdown", 8, 150, 600), ("pointerdown", 9, 200, 620), | |
| ("pointermove", 8, 150, 450), ("pointermove", 9, 200, 470), | |
| ("pointermove", 7, 200, 400, "pen"), ("pointerup", 7, 200, 400, "pen"), | |
| ("pointerup", 8, 150, 450), ("pointerup", 9, 200, 470)]) | |
| assert alpha_near(page, 160, 400) > 0 and alpha_near(page, 130, 600) > 0 | |
| # the hand rests first, then the pen comes down: the hand's mark is taken back, the pen writes | |
| page.wait_for_timeout(600) | |
| touch(page, [("pointerdown", 10, 60, 300), ("pointermove", 10, 120, 300), | |
| ("pointerdown", 11, 60, 250, "pen"), ("pointermove", 11, 200, 250, "pen"), ("pointerup", 11, 200, 250, "pen"), | |
| ("pointermove", 10, 200, 300), ("pointerup", 10, 200, 300)]) | |
| assert alpha_near(page, 130, 250) > 0 and alpha_near(page, 90, 300) == 0 | |
| page.locator(".mx-board-back").click() # back to the question; the sketch waits | |
| playwright.expect(page.locator(".mx-board")).to_have_count(0) | |
| playwright.expect(page.locator(".mx-sheet")).to_be_visible() | |
| page.locator("[data-board-toggle]").click() | |
| assert alpha_near(page, 150, 200) > 0 | |
| page.keyboard.press("Escape") # closes the board, not the exercise | |
| playwright.expect(page.locator(".mx-board")).to_have_count(0) | |
| playwright.expect(page.locator(".mx-sheet")).to_be_visible() | |
| assert errors == [] | |
| def test_the_board_slides_over_the_exercise_like_a_closet_door(browser, server): | |
| page, errors = open_skill(browser, server, 1440, 900) | |
| page.locator("[data-board-toggle]").click() | |
| board, sheet = page.locator(".mx-board"), page.locator(".mx-sheet") | |
| door = page.locator(".mx-board-door") | |
| playwright.expect(door).to_be_visible() | |
| playwright.expect(door).to_have_attribute("aria-expanded", "false") | |
| shut = board.bounding_box() | |
| sheet_box = sheet.bounding_box() | |
| assert shut["x"] + shut["width"] < sheet_box["x"] # beside the exercise | |
| door.click() # a tap: all the way open, over the exercise, and never full screen | |
| playwright.expect(door).to_have_attribute("aria-expanded", "true") | |
| page.wait_for_timeout(400) | |
| wide = board.bounding_box() | |
| assert wide["x"] == shut["x"] and abs(wide["x"] + wide["width"] - (1440 - 16)) < 2 | |
| assert page.evaluate("!document.fullscreenElement") | |
| assert page.evaluate("document.elementFromPoint(1300, 450).closest('.mx-board') !== null") | |
| drag(page, (900, 300), (1200, 300)) # it draws on the part that slid open | |
| assert alpha_near(page, 1050, 300) > 0 | |
| door.click() # a tap again: back beside the exercise | |
| playwright.expect(door).to_have_attribute("aria-expanded", "false") | |
| page.wait_for_timeout(400) | |
| assert abs(board.bounding_box()["width"] - shut["width"]) < 2 | |
| # dragged by its handle, it stops where it is let go | |
| d = door.bounding_box() | |
| x, y = d["x"] + d["width"] / 2, d["y"] + d["height"] / 2 | |
| page.mouse.move(x, y) | |
| page.mouse.down() | |
| page.mouse.move(x + 250, y, steps=8) | |
| page.mouse.up() | |
| playwright.expect(door).to_have_attribute("aria-expanded", "true") | |
| assert abs(board.bounding_box()["width"] - shut["width"] - 250) < 3 | |
| assert errors == [] | |
| def test_board_turns_on_draws_undoes_and_is_remembered(browser, server): | |
| page, errors = open_skill(browser, server, 1440, 900) | |
| toggle = page.locator("[data-board-toggle]") | |
| playwright.expect(toggle).to_be_visible() | |
| playwright.expect(toggle).to_have_attribute("aria-pressed", "false") | |
| toggle.click() | |
| playwright.expect(page.locator(".mx-board")).to_be_visible() | |
| playwright.expect(page.locator(".mx-sheet")).to_have_class(re.compile(r"\bwith-board\b")) | |
| # the board and the exercise sit side by side, not on top of each other | |
| board, sheet = page.locator(".mx-board").bounding_box(), page.locator(".mx-sheet").bounding_box() | |
| assert board["x"] + board["width"] <= sheet["x"] | |
| draw_line(page) | |
| assert ink_pixels(page) > 50 | |
| page.keyboard.press("Control+z") | |
| assert ink_pixels(page) == 0 | |
| draw_line(page) | |
| page.locator(".mx-board [data-do=clear]").click() | |
| assert ink_pixels(page) == 0 | |
| page.locator(".mx-board [data-do=undo]").click() # a clear can be undone | |
| assert ink_pixels(page) > 50 | |
| # closed and opened again: the board is still on and the same question keeps its sketch | |
| page.locator(".mx-x").click() | |
| page.locator(".mx-star.st-next").click() | |
| playwright.expect(page.locator(".mx-board")).to_be_visible() | |
| assert ink_pixels(page) > 50 | |
| page.locator(".mx-board-bar [data-do=hide]").click() | |
| playwright.expect(page.locator(".mx-board")).to_have_count(0) | |
| playwright.expect(page.locator("[data-board-toggle]")).to_have_attribute("aria-pressed", "false") | |
| assert page.evaluate("localStorage.getItem('mathly.board')") == "off" | |
| assert errors == [] | |
| def alpha_near(page, x, y): | |
| """The most ink in the 3x3 board pixels around (x, y), in CSS pixels from the board's corner.""" | |
| return page.evaluate("""([x,y])=>{ const c=document.querySelector('.mx-board canvas'), d=devicePixelRatio||1; | |
| const px=c.getContext('2d').getImageData(Math.round(x*d)-1,Math.round(y*d)-1,3,3).data; let m=0; | |
| for(let i=3;i<px.length;i+=4) m=Math.max(m,px[i]); return m; }""", [x, y]) | |
| def drag(page, a, b, steps=10): | |
| box = page.locator(".mx-board canvas").bounding_box() | |
| page.mouse.move(box["x"] + a[0], box["y"] + a[1]) | |
| page.mouse.down() | |
| for k in range(1, steps + 1): | |
| page.mouse.move(box["x"] + a[0] + (b[0] - a[0]) * k / steps, box["y"] + a[1] + (b[1] - a[1]) * k / steps) | |
| page.mouse.up() | |
| def board_on(page): | |
| page.locator("[data-board-toggle]").click() | |
| playwright.expect(page.locator(".mx-board")).to_be_visible() | |
| def test_lines_snap_circles_and_a_bigger_eraser(browser, server): | |
| page, errors = open_skill(browser, server, 1440, 900) | |
| board_on(page) | |
| page.locator(".mx-board [data-tool=line]").click() | |
| drag(page, (100, 100), (300, 100)) | |
| assert alpha_near(page, 200, 100) > 0 | |
| drag(page, (305, 108), (305, 300)) # starts near the first line's end: it starts exactly there | |
| assert alpha_near(page, 300, 120) > 0 and alpha_near(page, 306, 120) == 0 | |
| page.locator(".mx-board [data-tool=circle]").click() | |
| drag(page, (450, 300), (530, 300)) # from the center out to the radius | |
| assert alpha_near(page, 450, 380) > 0 and alpha_near(page, 450, 300) == 0 | |
| # the eraser's sizes show only with the eraser; the big one wipes what the small one misses | |
| playwright.expect(page.locator(".mx-board-sizes")).to_be_hidden() | |
| page.locator(".mx-board [data-tool=eraser]").click() | |
| playwright.expect(page.locator(".mx-board-sizes")).to_be_visible() | |
| page.locator(".mx-board [data-eraser='16']").click() | |
| drag(page, (150, 125), (250, 125)) | |
| assert alpha_near(page, 200, 100) > 0 | |
| page.locator(".mx-board [data-eraser='90']").click() | |
| drag(page, (150, 125), (250, 125)) | |
| assert alpha_near(page, 200, 100) == 0 | |
| assert errors == [] | |
| def test_the_practice_exam_has_a_board_with_a_sketch_per_question(browser, server): | |
| page, errors = open_skill(browser, server, 1440, 900) | |
| page.locator(".mx-x").click() | |
| page.evaluate("""() => { const b = document.createElement('button'); b.dataset.act = 'quiz'; | |
| document.getElementById('mathView').append(b); b.click(); b.remove(); }""") # the mixed quiz, as an assignment opens it | |
| page.locator("[data-act=quiz-start]").click() | |
| playwright.expect(page.locator(".mx-quiz-sheet")).to_be_visible() | |
| board_on(page) | |
| draw_line(page) | |
| assert ink_pixels(page) > 50 | |
| page.locator("[data-act=quiz-next]").click() | |
| playwright.expect(page.locator(".mx-board")).to_be_visible() # still on for the next question, on a clean page | |
| assert ink_pixels(page) == 0 | |
| page.locator("[data-act=quiz-prev]").click() | |
| assert ink_pixels(page) > 50 | |
| assert errors == [] | |
| def test_elements_move_together_when_joined_and_can_be_deleted(browser, server): | |
| page, errors = open_skill(browser, server, 1440, 900) | |
| board_on(page) | |
| page.locator(".mx-board [data-tool=line]").click() | |
| drag(page, (100, 300), (300, 300)) | |
| drag(page, (302, 302), (200, 150)) # snaps to the first line's end | |
| drag(page, (198, 148), (102, 298)) # and closes the triangle | |
| page.locator(".mx-board [data-tool=circle]").click() | |
| drag(page, (500, 250), (550, 250)) | |
| page.locator(".mx-board [data-tool=move]").click() | |
| drag(page, (200, 300), (200, 400)) # grab the base: the whole triangle moves | |
| assert alpha_near(page, 200, 300) == 0 and alpha_near(page, 200, 400) > 0 | |
| assert alpha_near(page, 200, 250) > 0 # the top corner came along (was at 150) | |
| assert alpha_near(page, 550, 250) > 0 # the circle stayed | |
| drag(page, (550, 250), (550, 450)) # the circle alone | |
| assert alpha_near(page, 550, 250) == 0 and alpha_near(page, 550, 450) > 0 | |
| playwright.expect(page.locator(".mx-board-del")).to_be_visible() | |
| page.keyboard.press("Delete") | |
| assert alpha_near(page, 550, 450) == 0 | |
| page.keyboard.press("Control+z") # a delete can be undone, and so can a move | |
| assert alpha_near(page, 550, 450) > 0 | |
| page.keyboard.press("Control+z") | |
| assert alpha_near(page, 550, 250) > 0 | |
| assert errors == [] | |
| def test_the_eraser_cuts_a_pen_stroke_and_a_moved_shape_comes_whole(browser, server): | |
| page, errors = open_skill(browser, server, 1440, 900) | |
| board_on(page) | |
| drag(page, (100, 200), (500, 200), steps=30) # a pen stroke | |
| page.locator(".mx-board [data-tool=eraser]").click() | |
| page.locator(".mx-board [data-eraser='40']").click() | |
| drag(page, (300, 150), (300, 250)) # cut through the middle | |
| assert alpha_near(page, 300, 200) == 0 | |
| assert alpha_near(page, 150, 200) > 0 and alpha_near(page, 450, 200) > 0 | |
| page.locator(".mx-board [data-tool=move]").click() | |
| drag(page, (150, 200), (150, 400)) # the left piece is an element of its own | |
| assert alpha_near(page, 150, 400) > 0 and alpha_near(page, 450, 200) > 0 | |
| assert errors == [] | |
| def test_the_question_is_copied_to_the_top_of_the_board(browser, server): | |
| page, errors = open_skill(browser, server, 1440, 900) | |
| board_on(page) | |
| width = page.locator(".mx-board canvas").bounding_box()["width"] | |
| assert alpha_near(page, width - 40, 30) == 0 | |
| page.locator(".mx-board [data-do=question]").click() | |
| assert alpha_near(page, width - 40, 30) > 0 # a card at the top | |
| playwright.expect(page.locator(".mx-board [data-tool=move]")).to_have_attribute("aria-pressed", "true") | |
| drag(page, (width - 40, 30), (width - 40, 430)) # it moves like anything else | |
| assert alpha_near(page, width - 40, 30) == 0 and alpha_near(page, width - 40, 430) > 0 | |
| page.locator(".mx-board-del").click() | |
| assert ink_pixels(page) == 0 | |
| assert errors == [] | |
| def test_the_wheel_scrolls_an_endless_paper(browser, server): | |
| page, errors = open_skill(browser, server, 1440, 900) | |
| board_on(page) | |
| drag(page, (100, 100), (300, 100)) | |
| box = page.locator(".mx-board canvas").bounding_box() | |
| page.mouse.move(box["x"] + 300, box["y"] + 300) | |
| page.mouse.wheel(0, -500) # never above the top | |
| page.wait_for_timeout(100) | |
| assert alpha_near(page, 200, 100) > 0 | |
| page.mouse.wheel(0, 2000) | |
| page.wait_for_timeout(100) | |
| assert alpha_near(page, 200, 100) == 0 # scrolled out of sight | |
| playwright.expect(page.locator(".mx-board-top")).to_be_visible() | |
| drag(page, (100, 300), (300, 300)) # drawn far down the paper | |
| page.locator(".mx-board-top").click() | |
| assert alpha_near(page, 200, 100) > 0 and alpha_near(page, 200, 300) == 0 | |
| page.mouse.move(box["x"] + 300, box["y"] + 300) | |
| page.mouse.wheel(0, 2000) | |
| page.wait_for_timeout(100) | |
| assert alpha_near(page, 200, 300) > 0 | |
| playwright.expect(page.locator(".mx-board-send")).to_be_hidden() # handing in is the unit quiz's | |
| assert errors == [] | |
| def type_math(page, at, latex): | |
| """Taps the board with the math tool at `at`, types `latex` into the math field and adds it.""" | |
| box = page.locator(".mx-board canvas").bounding_box() | |
| page.mouse.click(box["x"] + at[0], box["y"] + at[1]) | |
| field = page.locator(".mx-board-mathedit math-field") | |
| playwright.expect(field).to_be_visible(timeout=10000) | |
| page.evaluate("v=>{document.querySelector('.mx-board-mathedit math-field').value=v}", latex) | |
| page.locator(".mx-board-mathedit [data-math=ok]").dispatch_event("pointerdown") | |
| playwright.expect(page.locator(".mx-board-mathedit")).to_have_count(0) | |
| def test_typed_math_is_an_element_like_any_other(browser, server): | |
| page, errors = open_skill(browser, server, 1440, 900) | |
| board_on(page) | |
| page.locator(".mx-board [data-tool=math]").click() | |
| box = page.locator(".mx-board canvas").bounding_box() | |
| page.mouse.click(box["x"] + 100, box["y"] + 200) | |
| playwright.expect(page.locator(".mx-board-mathedit math-field")).to_be_visible(timeout=10000) | |
| assert page.evaluate("window.mathVirtualKeyboard.visible") # the math keyboard opens with it | |
| page.keyboard.press("Escape") # cancels the formula, not the board | |
| playwright.expect(page.locator(".mx-board-mathedit")).to_have_count(0) | |
| playwright.expect(page.locator(".mx-board")).to_be_visible() | |
| assert ink_pixels(page) == 0 | |
| type_math(page, (100, 200), r"\frac{x^2-9}{x+3}=x-3") | |
| assert ink_pixels(page) > 100 | |
| assert alpha_near(page, 112, 200) > 0 or alpha_near(page, 130, 200) > 0 | |
| type_math(page, (100, 200), r"x^2") # tapping it with the math tool edits it, in place | |
| assert ink_pixels(page) > 0 | |
| page.locator(".mx-board [data-do=undo]").click() # back to the fraction | |
| page.locator(".mx-board [data-tool=move]").click() | |
| drag(page, (120, 200), (120, 450)) | |
| assert alpha_near(page, 112, 200) + alpha_near(page, 130, 200) == 0 | |
| assert alpha_near(page, 112, 450) + alpha_near(page, 130, 450) > 0 # it went with the drag, whole | |
| page.locator(".mx-board [data-tool=eraser]").click() | |
| drag(page, (100, 440), (260, 460)) | |
| assert ink_pixels(page) == 0 # the eraser takes the whole formula | |
| assert errors == [] | |
| def test_the_math_renderer_draws_what_the_keyboard_makes(browser, server): | |
| page, errors = open_skill(browser, server, 1440, 900) | |
| sizes = page.evaluate("""()=>[ | |
| 'x^2-5x+6=0', 'x_{1,2}=\\\\frac{-b\\\\pm\\\\sqrt{b^2-4ac}}{2a}', '\\\\sqrt[3]{8}', '\\\\left(\\\\frac{a}{b}\\\\right)^{3}', | |
| '\\\\left|x-2\\\\right|\\\\le 5', '\\\\angle ABC=30^{\\\\circ}', '\\\\triangle ABC\\\\cong\\\\triangle DEF', 'a\\\\perp b\\\\parallel c', | |
| '\\\\text{שטח}=\\\\pi r^2', '\\\\placeholder{}+1', '\\\\unknowncommand{x}', '' | |
| ].map(l=>{ const L=window.MathlyBoardMath.layout(l,26); return [L.w,L.a,L.d]; })""") | |
| assert all(w >= 0 and a >= 0 and d >= 0 for w, a, d in sizes) | |
| assert all(w > 10 for w, _, _ in sizes[:-1]) | |
| frac, plain = sizes[1], sizes[0] | |
| assert frac[0] > plain[0] and frac[1] > plain[1] # a fraction is wider and taller than a line of text | |
| assert errors == [] | |
| def test_the_move_tool_scrolls_when_it_drags_empty_paper(browser, server): | |
| page, errors = open_skill(browser, server, 390, 844) | |
| page.locator("[data-board-toggle]").click() | |
| touch(page, [("pointerdown", 1, 60, 300), *[("pointermove", 1, 60 + k * 20, 300) for k in range(1, 10)], ("pointerup", 1, 240, 300)]) | |
| page.locator(".mx-board [data-tool=move]").click() | |
| # one finger on empty paper: the paper scrolls, the line goes up with it | |
| touch(page, [("pointerdown", 2, 150, 600), *[("pointermove", 2, 150, 600 - k * 20) for k in range(1, 11)], ("pointerup", 2, 150, 400)]) | |
| assert alpha_near(page, 150, 300) == 0 and alpha_near(page, 150, 100) > 0 | |
| playwright.expect(page.locator(".mx-board-top")).to_be_visible() | |
| # one finger on the line: the line moves, the paper stays | |
| touch(page, [("pointerdown", 3, 150, 100), *[("pointermove", 3, 150, 100 + k * 20) for k in range(1, 11)], ("pointerup", 3, 150, 300)]) | |
| assert alpha_near(page, 150, 100) == 0 and alpha_near(page, 150, 300) > 0 | |
| playwright.expect(page.locator(".mx-board-del")).to_be_visible() # it is selected, not the paper scrolled | |
| # never above the top | |
| touch(page, [("pointerdown", 4, 300, 200), *[("pointermove", 4, 300, 200 + k * 60) for k in range(1, 11)], ("pointerup", 4, 300, 800)]) | |
| playwright.expect(page.locator(".mx-board-top")).to_be_hidden() | |
| assert errors == [] | |
| def test_an_angle_gets_an_arc_and_its_words_at_a_triangles_corner(browser, server): | |
| page, errors = open_skill(browser, server, 1440, 900) | |
| board_on(page) | |
| page.locator(".mx-board [data-tool=line]").click() | |
| drag(page, (100, 300), (300, 300)) | |
| drag(page, (302, 302), (200, 150)) | |
| drag(page, (198, 148), (102, 298)) | |
| page.locator(".mx-board [data-tool=angle]").click() | |
| box = page.locator(".mx-board canvas").bounding_box() | |
| at = lambda x, y: (box["x"] + x, box["y"] + y) | |
| # the bottom-left corner: its edges go right (0°) and up to the top (-56°), so the arc is at radius 30, around -28° | |
| arc, words = (126, 286), (138, 280) | |
| ink_by = lambda x, y: max(alpha_near(page, x + dx, y + dy) for dx in (-6, -3, 0, 3, 6) for dy in (-4, 0, 4)) | |
| assert alpha_near(page, *arc) == 0 | |
| page.mouse.move(*at(112, 294)) # pointing into the corner: its arc shows, dashed, before anything is added | |
| assert alpha_near(page, *arc) > 0 | |
| page.mouse.move(*at(400, 500)) # away from every corner: nothing | |
| assert alpha_near(page, *arc) == 0 | |
| page.mouse.click(*at(112, 294)) | |
| field = page.locator(".mx-board-anglein") | |
| playwright.expect(field).to_be_focused() | |
| field.fill("60") | |
| field.press("Enter") | |
| playwright.expect(field).to_have_count(0) | |
| assert alpha_near(page, *arc) > 0 and ink_by(*words) > 0 | |
| # the words open again for a change; a number is shown without its degree sign, which comes back by itself | |
| page.mouse.click(*at(*words)) | |
| playwright.expect(field).to_have_value("60") | |
| field.press("Escape") # leaves it as it was, and the board open | |
| playwright.expect(field).to_have_count(0) | |
| playwright.expect(page.locator(".mx-board")).to_be_visible() | |
| assert ink_by(*words) > 0 | |
| # the angle belongs to its corner: it moves with the triangle and goes with an undo | |
| page.locator(".mx-board [data-tool=move]").click() | |
| drag(page, (200, 300), (200, 400)) | |
| assert alpha_near(page, *arc) == 0 and alpha_near(page, arc[0], arc[1] + 100) > 0 | |
| page.keyboard.press("Control+z") # the move | |
| page.keyboard.press("Control+z") # the angle | |
| assert alpha_near(page, *arc) == 0 and alpha_near(page, 200, 300) > 0 | |
| # a corner where nothing meets is no angle; Escape on a new one takes it back | |
| page.locator(".mx-board [data-tool=angle]").click() | |
| page.mouse.click(*at(500, 500)) | |
| playwright.expect(field).to_have_count(0) | |
| page.mouse.click(*at(112, 294)) | |
| field.press("Escape") | |
| assert alpha_near(page, *arc) == 0 | |
| # α, β and γ are a tap away, typed where the cursor is; a number added to them is in degrees too | |
| page.mouse.click(*at(112, 294)) | |
| page.locator(".mx-board-angleedit button", has_text="β").click() | |
| playwright.expect(field).to_have_value("β") | |
| playwright.expect(field).to_be_focused() | |
| page.keyboard.type("+90") | |
| page.keyboard.press("Enter") | |
| playwright.expect(field).to_have_count(0) | |
| assert alpha_near(page, *arc) > 0 and ink_by(*words) > 0 | |
| page.mouse.click(*at(*words)) | |
| playwright.expect(field).to_have_value("β+90") # its degree sign comes back by itself | |
| field.fill("180-alpha") # the letters' names become the letters | |
| field.press("Enter") | |
| page.mouse.click(*at(*words)) | |
| playwright.expect(field).to_have_value("180-α") | |
| # a double arc (equal angles are marked alike): a second arc 4 px outside the first, kept, and undone like the rest | |
| outer = (100 + 34 * 0.883, 300 - 34 * 0.469) | |
| assert alpha_near(page, *outer) == 0 | |
| page.locator(".mx-board-arcs [data-arcs='2']").click() | |
| assert alpha_near(page, *outer) > 0 # shown at once | |
| field.press("Enter") | |
| assert alpha_near(page, *arc) > 0 and alpha_near(page, *outer) > 0 | |
| page.mouse.click(*at(112, 294)) | |
| playwright.expect(page.locator(".mx-board-arcs [data-arcs='2']")).to_have_attribute("aria-pressed", "true") | |
| field.press("Escape") | |
| page.keyboard.press("Control+z") | |
| assert alpha_near(page, *arc) > 0 and alpha_near(page, *outer) == 0 | |
| # its colour, arcs and words together, picked in the same box | |
| red = lambda: page.evaluate("""([x,y])=>{ const c=document.querySelector('.mx-board canvas'), d=devicePixelRatio||1; | |
| const px=c.getContext('2d').getImageData(Math.round(x*d)-1,Math.round(y*d)-1,3,3).data; let n=0; | |
| for(let i=0;i<px.length;i+=4) if(px[i+3]>128&&px[i]>150&&px[i+2]<130) n++; return n; }""", list(arc)) | |
| assert red() == 0 | |
| page.mouse.click(*at(112, 294)) | |
| page.locator(".mx-board-colors [data-color='#D3455B']").click() | |
| assert red() > 0 # shown at once | |
| field.press("Enter") | |
| assert red() > 0 | |
| page.keyboard.press("Control+z") | |
| assert red() == 0 | |
| assert errors == [] | |
| def test_angles_where_a_line_meets_a_side_or_crosses_another(browser, server): | |
| page, errors = open_skill(browser, server, 1440, 900) | |
| board_on(page) | |
| page.locator(".mx-board [data-tool=line]").click() | |
| drag(page, (90, 438), (420, 438)) | |
| drag(page, (92, 436), (200, 80)) | |
| drag(page, (202, 82), (418, 436)) | |
| drag(page, (200, 82), (262, 433)) # from the top down to the base, ending a little short: it snaps onto the base | |
| drag(page, (120, 300), (380, 300)) # across the triangle | |
| page.locator(".mx-board [data-tool=angle]").click() | |
| box = page.locator(".mx-board canvas").bounding_box() | |
| field = page.locator(".mx-board-anglein") | |
| def add(x, y, text): | |
| page.mouse.click(box["x"] + x, box["y"] + y) | |
| playwright.expect(field).to_be_focused() | |
| field.fill(text) | |
| field.press("Enter") | |
| foot = 262 + (438 - 433) * 62 / 353 # where the middle line meets the base | |
| assert alpha_near(page, foot, 437) > 0 and alpha_near(page, foot, 441) == 0 # it reached the base, not past it | |
| add(250, 430, "α") # either side of the line drawn from the base | |
| add(275, 430, "β") | |
| assert alpha_near(page, foot - 30, 438 - 3) > 0 and alpha_near(page, foot + 30, 438 - 3) > 0 | |
| add(191, 106, "x") # the two halves of the top corner | |
| add(211, 104, "y") | |
| add(205, 105, "2α+β") # pointing along the middle line: the whole top angle, a wider arc | |
| assert page.locator(".mx-board-anglein").count() == 0 | |
| # a narrow half's words sit right by its arc (radius 30), not far down between the lines; the whole angle's arc | |
| # goes around them | |
| x_words = (200 + 45 * -0.07, 80 + 45 * 1.0) | |
| assert max(alpha_near(page, x_words[0] + dx, x_words[1] + dy) for dx in (-4, 0, 4) for dy in (-4, 0, 4)) > 0 | |
| add(250, 306, "60") # where two lines cross | |
| # it all moves as one: the line from the base and the crossing line come with the triangle, their angles too | |
| page.locator(".mx-board [data-tool=move]").click() | |
| drag(page, (150, 438), (150, 538)) | |
| assert alpha_near(page, foot - 30, 535) > 0 and alpha_near(page, foot - 30, 435) == 0 | |
| assert alpha_near(page, 300, 400) > 0 # the crossing line, 100 lower | |
| assert errors == [] | |
| def test_a_right_angle_is_marked_with_a_square(browser, server): | |
| page, errors = open_skill(browser, server, 1440, 900) | |
| board_on(page) | |
| page.locator(".mx-board [data-tool=line]").click() | |
| drag(page, (100, 400), (340, 400)) | |
| drag(page, (102, 398), (100, 160)) | |
| drag(page, (100, 162), (338, 398)) | |
| page.locator(".mx-board [data-tool=angle]").click() | |
| box = page.locator(".mx-board canvas").bounding_box() | |
| page.mouse.click(box["x"] + 112, box["y"] + 388) | |
| page.locator(".mx-board-arcs [data-arcs='right']").click() | |
| page.locator(".mx-board-anglein").press("Enter") | |
| # a square of side 14 in the corner: its far corner at (114, 386), and no arc at radius 30 | |
| assert alpha_near(page, 114, 386) > 0 and alpha_near(page, 107, 386) > 0 and alpha_near(page, 114, 393) > 0 | |
| assert alpha_near(page, 100 + 30 * 0.707, 400 - 30 * 0.707) == 0 | |
| page.mouse.click(box["x"] + 112, box["y"] + 388) # the square opens it again, as an arc does | |
| playwright.expect(page.locator(".mx-board-arcs [data-arcs='right']")).to_have_attribute("aria-pressed", "true") | |
| page.locator(".mx-board-arcs [data-arcs='1']").click() | |
| page.locator(".mx-board-anglein").press("Enter") | |
| assert alpha_near(page, 114, 386) == 0 and alpha_near(page, 100 + 30 * 0.707, 400 - 30 * 0.707) > 0 | |
| assert errors == [] | |
| def test_the_board_has_a_scientific_calculator(browser, server): | |
| page, errors = open_skill(browser, server, 1400, 900) | |
| page.locator("[data-board-toggle]").click() | |
| page.locator('.mx-board [data-do="calc"]').click() | |
| calc = page.locator(".mx-calc") | |
| playwright.expect(calc).to_be_visible() | |
| def tap(*labels): | |
| for label in labels: | |
| calc.locator("button", has=page.locator(f"b:text-is('{label}')")).first.click() | |
| tap("sin", "3", "0", ")", "=") | |
| playwright.expect(calc.locator("output")).to_have_text("0.5") | |
| tap("S⇔D") | |
| playwright.expect(calc.locator("output")).to_have_text("1/2") | |
| tap("×", "4", "=") # carries on from the answer | |
| playwright.expect(calc.locator("output")).to_have_text("2") | |
| tap("AC", "SHIFT", "sin", "1", ")", "=") # sin⁻¹(1) in degrees | |
| playwright.expect(calc.locator("output")).to_have_text("90") | |
| tap("AC", "2", "÷", "0", "=") | |
| playwright.expect(calc.locator("output")).to_have_text("שגיאה") | |
| assert not errors | |
| def test_the_calculator_is_dragged_by_its_bar(browser, server): | |
| page, errors = open_skill(browser, server, 1400, 900) | |
| page.locator("[data-board-toggle]").click() | |
| page.locator('.mx-board [data-do="calc"]').click() | |
| calc = page.locator(".mx-calc") | |
| playwright.expect(calc).to_be_visible() | |
| stage = page.locator(".mx-board-stage").bounding_box() | |
| before = calc.bounding_box() | |
| grip = page.locator(".mx-calc-grip").bounding_box() | |
| gx, gy = grip["x"] + grip["width"] / 2, grip["y"] + grip["height"] / 2 | |
| page.mouse.move(gx, gy) | |
| page.mouse.down() | |
| for k in range(1, 11): | |
| page.mouse.move(gx + k * 20, gy - k * 15) | |
| page.mouse.up() | |
| after = calc.bounding_box() | |
| assert abs(after["x"] - before["x"] - 200) < 2 and abs(after["y"] - before["y"] + 150) < 2 | |
| # dragged far past the edge: it stays whole inside the board | |
| grip = page.locator(".mx-calc-grip").bounding_box() | |
| gx, gy = grip["x"] + grip["width"] / 2, grip["y"] + grip["height"] / 2 | |
| page.mouse.move(gx, gy) | |
| page.mouse.down() | |
| page.mouse.move(gx - 2000, gy - 2000, steps=5) | |
| page.mouse.up() | |
| box = calc.bounding_box() | |
| assert abs(box["x"] - stage["x"]) < 2 and abs(box["y"] - stage["y"]) < 2 | |
| # closed and opened again: it comes back where it was left | |
| page.locator('.mx-board [data-do="calc"]').click() | |
| page.locator('.mx-board [data-do="calc"]').click() | |
| again = calc.bounding_box() | |
| assert abs(again["x"] - box["x"]) < 2 and abs(again["y"] - box["y"]) < 2 | |
| assert errors == [] | |
| def test_the_paper_scrolls_sideways_to_what_was_written_on_the_wide_board(browser, server): | |
| page, errors = open_skill(browser, server, 1440, 900) | |
| page.locator("[data-board-toggle]").click() | |
| door = page.locator(".mx-board-door") | |
| door.click() | |
| page.wait_for_timeout(400) | |
| drag(page, (1100, 300), (1300, 300)) # written on the part that slid open | |
| assert alpha_near(page, 1200, 300) > 0 | |
| door.click() # slid back: that writing is past the board's right edge | |
| page.wait_for_timeout(400) | |
| width = page.locator(".mx-board canvas").bounding_box()["width"] | |
| assert width < 1100 | |
| # two fingers moving left bring it into sight | |
| touch(page, [("pointerdown", 1, 500, 400), ("pointerdown", 2, 500, 500), | |
| *[e for k in range(1, 11) for e in (("pointermove", 1, 500 - k * 60, 400), ("pointermove", 2, 500 - k * 60, 500))], | |
| ("pointerup", 1, -100, 400), ("pointerup", 2, -100, 500)]) | |
| assert alpha_near(page, 600, 300) > 0 | |
| playwright.expect(page.locator(".mx-board-top")).to_be_visible() | |
| # Shift and the wheel scroll sideways too, never past the paper's left edge | |
| box = page.locator(".mx-board canvas").bounding_box() | |
| page.mouse.move(box["x"] + 200, box["y"] + 200) | |
| page.keyboard.down("Shift") | |
| page.mouse.wheel(0, -2000) | |
| page.keyboard.up("Shift") | |
| page.wait_for_timeout(100) | |
| assert alpha_near(page, 600, 300) == 0 # back at the left edge: the writing is out of sight again | |
| # the move tool drags the paper sideways | |
| page.locator('.mx-board [data-tool="move"]').click() | |
| drag(page, (700, 600), (100, 600)) | |
| assert alpha_near(page, 600, 300) > 0 | |
| page.locator(".mx-board-top").click() # back to the paper's corner | |
| assert alpha_near(page, 600, 300) == 0 | |
| playwright.expect(page.locator(".mx-board-top")).to_be_hidden() | |
| assert errors == [] | |
| def test_a_held_pen_does_not_select_the_page(browser, server): | |
| page, errors = open_skill(browser, server, 390, 844) | |
| page.locator("[data-board-toggle]").click() | |
| playwright.expect(page.locator(".mx-board.full")).to_be_visible() | |
| style = page.evaluate("""()=>{ const s=getComputedStyle(document.querySelector('.mx-board')); | |
| return [s.userSelect||s.webkitUserSelect, s.webkitTouchCallout||'none']; }""") | |
| assert style[0] == "none" | |
| # nothing on the board starts a selection, and the paper's touches are its own | |
| blocked = page.evaluate("""()=>{ const b=document.querySelector('.mx-board'); | |
| const sel=new Event('selectstart',{bubbles:true,cancelable:true}); b.querySelector('[data-title]').dispatchEvent(sel); | |
| const t=new Event('touchstart',{bubbles:true,cancelable:true}); b.querySelector('canvas').dispatchEvent(t); | |
| return [sel.defaultPrevented, t.defaultPrevented]; }""") | |
| assert blocked == [True, True] | |
| # a field to type in still selects | |
| page.evaluate("""()=>{ const i=document.createElement('input'); i.id='probe'; document.querySelector('.mx-board-stage').append(i); }""") | |
| assert not page.evaluate("""()=>{ const e=new Event('selectstart',{bubbles:true,cancelable:true}); | |
| document.getElementById('probe').dispatchEvent(e); return e.defaultPrevented; }""") | |
| assert errors == [] | |