"""Drive Wonder Ink's print preview through one scenario, headless. usage: wonderink-print-rig.py WONDERINK_COMMAND SCENARIO Run by tools/lab/wonderink-print-check inside the lab, against the command the package installed. It presses the preview's own buttons the way a teacher would, and prints what happened; the check around it asks CUPS what actually arrived. """ import importlib.machinery, importlib.util, sys, time, faulthandler, gc, os faulthandler.enable() path, scenario = sys.argv[1], sys.argv[2] loader = importlib.machinery.SourceFileLoader("wonderink", path) spec = importlib.util.spec_from_loader("wonderink", loader) wi = importlib.util.module_from_spec(spec); loader.exec_module(wi) from pathlib import Path from PyQt6.QtCore import QTimer, QUrl from PyQt6.QtGui import QPdfWriter, QPainter, QFont, QPageSize from PyQt6.QtWidgets import QApplication from PyQt6.QtWebEngineCore import QWebEngineProfile, QWebEngineSettings notes = [] wi.notify = lambda s, b: (notes.append(s), print("NOTIFY", s, "|", b, flush=True)) app = QApplication(sys.argv) # Wonder Ink's own profile, settings and all — not one of the rig's. profile = wi.build_profile(app, "rig") window = wi.WonderInk(profile) profile.downloadRequested.connect(window.on_download) window.resize(1280, 800); window.show() t0 = time.monotonic() def stamp(m): print(f"[{time.monotonic()-t0:6.2f}s] {m}", flush=True) def lesson(view, title="Noahs Ark lesson"): view.setHtml(f"{title}" + "".join( f"

{title}, part {i}

{'Noah built an ark. ' * 80}

" for i in range(5) ) + "", QUrl("https://app.wonderink.org/lesson")) def worksheet(name="Colour the Ark"): d = wi.DOWNLOADS; d.mkdir(parents=True, exist_ok=True) p = d / f"{name}.pdf" w = QPdfWriter(str(p)); w.setPageSize(QPageSize(QPageSize.PageSizeId.Letter)); w.setResolution(300) pa = QPainter(w); pa.setFont(QFont("DejaVu Sans", 28)) pa.drawText(200, 400, f"WORKSHEET {name} page one"); pa.drawRect(200, 600, 2000, 1500) w.newPage(); pa.drawText(200, 400, f"WORKSHEET {name} page two"); pa.end() return p def when(cond, then, label, limit=30): start = time.monotonic() def poll(): try: ok = cond() except Exception as e: ok = False if ok: return then() if time.monotonic() - start > limit: stamp(f"GAVE UP waiting for {label}"); app.exit(4); return QTimer.singleShot(100, poll) QTimer.singleShot(100, poll) def preview(): return window._preview def ready(): return preview() is not None and preview()._pdf is not None def describe(d): return f"picture={d._picture_box.isVisible()} shown={(d._pdf, d._page) in d._images} pages={d._pages} printers={[d._printer.itemText(i) for i in range(d._printer.count())]} paper={d.paper()} colour_offered={d._colour.isVisible()} status={d._status.text()!r}" def shown_gray(): """Is the page the preview is drawing grey, pixel for pixel?""" pixmap = preview()._view._pixmap return None if pixmap is None else pixmap.toImage().allGray() def keep_preview(): import shutil out = Path("/tmp/rig-out"); out.mkdir(exist_ok=True) shutil.copy(preview()._pdf, out / "preview.pdf") def toggles(sequence, then): """Color (0) and Black and white (1) in turn, as a teacher would, each shown before the next.""" def step(i): if i == len(sequence): return then() start = time.monotonic(); preview()._colour.setCurrentIndex(sequence[i]) def shown(): d = preview() stamp(f"TOGGLE {i} want={'gray' if sequence[i] else 'colour'} took={time.monotonic() - start:.2f} " f"pdf={d._pdf.name} source={d._source.name} page={d._page}/{d._pages} shown_gray={shown_gray()}") step(i + 1) when(ready, shown, f"toggle {i}") step(0) def colour_lesson(view, title="Rainbow lesson"): # Backgrounds do not print, so the colour is in the words and the drawings. shapes = ("" "" "") view.setHtml(f"{title}" + "".join( f"

{title}, part {i}

{shapes}" f"

{'God put a rainbow in the sky. ' * 60}

" for i in range(4) ) + "", QUrl("https://app.wonderink.org/rainbow")) def colour_worksheet(name="Colour the Rainbow"): from PyQt6.QtGui import QColor d = wi.DOWNLOADS; d.mkdir(parents=True, exist_ok=True) p = d / f"{name}.pdf" w = QPdfWriter(str(p)); w.setPageSize(QPageSize(QPageSize.PageSizeId.Letter)); w.setResolution(300) pa = QPainter(w); pa.setFont(QFont("DejaVu Sans", 28)) for page, colours in enumerate((("#e00000", "#ff9900"), ("#00a000", "#0050e0"))): if page: w.newPage() pa.setPen(QColor(colours[0])); pa.drawText(200, 400, f"WORKSHEET {name} page {page + 1}") pa.fillRect(200, 600, 1000, 800, QColor(colours[1])) pa.end() return p def finish(code=0): stamp(f"RESULT notes={notes} preview_open={preview() is not None} tabs={window._tabs.count()}") QTimer.singleShot(1500, lambda: app.exit(code)) def press_print(then=None): d = preview(); stamp("PREVIEW " + describe(d)) d._print.click() stamp(f"after Print: open={preview() is not None} status={d._status.text() if preview() else ''!r}") (then or finish)() actions = { "web": lambda view: when(ready, press_print, "preview"), "cancel": lambda view: when(ready, lambda: (stamp("PREVIEW " + describe(preview())), preview()._cancel.click(), finish()), "preview"), "options": lambda view: when(ready, lambda: ( preview()._which.setCurrentIndex(1), preview()._range.setText("2-3"), preview()._copies.setValue(2), preview()._colour.setCurrentIndex(1), when(ready, press_print, "black and white")), "preview"), "bad-range": lambda view: when(ready, lambda: ( preview()._which.setCurrentIndex(1), preview()._range.setText("7"), preview()._print.click(), stamp(f"bad range: open={preview() is not None} status={preview()._status.text()!r}"), preview()._range.setText("1"), press_print()), "preview"), "broken": lambda view: when(ready, lambda: ( preview()._printer.setCurrentText("Refusing"), stamp(f"switched to Refusing: paper={preview().paper()} status={preview()._status.text()!r}"), when(ready, lambda: ( stamp(f"re-rendered on {preview().paper()}: pages={preview()._pages}"), preview()._print.click(), stamp(f"broken: open={preview() is not None} status={preview()._status.text()!r}"), preview()._cancel.click(), finish()), "re-render")), "preview"), "page-closes-itself": lambda view: when(ready, lambda: ( view.page().runJavaScript("window.close()"), when(lambda: preview() and "closed" in preview()._status.text(), lambda: ( stamp(f"after page closed: status={preview()._status.text()!r} print_enabled={preview()._print.isEnabled()}"), preview()._cancel.click(), finish()), "closed message")), "preview"), "close-window": lambda view: when(ready, lambda: (stamp("closing window with preview open"), window.close()), "preview"), "page-prints-again": lambda view: when(ready, lambda: ( view.page().runJavaScript("window.print()"), QTimer.singleShot(800, lambda: (stamp(f"second request while open: still one preview={preview() is not None}"), press_print()))), "preview"), } SITE = "http://127.0.0.1:8094" if scenario.startswith("bw-"): # bw-web: a colourful lesson, turned to page 2, Color and Black and white # back and forth, then page 2 twice in black and white. # bw-pdf: a colourful worksheet, all of it, in black and white. # bw-colour-again: black and white, then back to Color, then Print. # bw-cancel: black and white, then Cancel. if scenario == "bw-pdf": window.open_pdf(colour_worksheet()); view = window.current() else: view = window.open_tab(); colour_lesson(view) def colour_first(): stamp(f"COLOUR FIRST shown_gray={shown_gray()} pdf={preview()._pdf.name}") if scenario == "bw-web": preview()._turn(1) toggles([1, 0, 1, 0, 1], lambda: ( preview()._which.setCurrentIndex(1), preview()._range.setText("2"), preview()._copies.setValue(2), keep_preview(), press_print())) elif scenario == "bw-pdf": toggles([1], lambda: (keep_preview(), press_print())) elif scenario == "bw-colour-again": toggles([1, 0], lambda: (keep_preview(), press_print())) else: toggles([1], lambda: (stamp("PREVIEW " + describe(preview())), preview()._cancel.click(), finish())) begin = lambda *_: (QTimer.singleShot(0, lambda: window.print_view(view)), when(ready, colour_first, "preview")) if scenario == "bw-pdf": QTimer.singleShot(2500, begin) else: view.loadFinished.connect(begin) elif scenario.startswith("links:"): # links:follow every kind of link on the lab's signed-in links page, # clicked in turn: what opens where, and what does not # links:pdf a worksheet reached by a link, printed # links:image a picture reached by a link, printed as a picture # links:image-rclick the same picture, right-click Print image # links:tabs-print several links opened, some closed, then the page printed from PyQt6.QtCore import QEvent, QPointF, Qt from PyQt6.QtGui import QMouseEvent from PyQt6.QtWidgets import QApplication as Application variant = scenario.split(":", 1)[1] links_view = window.open_tab(); links_view.load(QUrl(f"{SITE}/login")) def on_links(): return links_view.url().path() == "/lesson" or links_view.url().path() == "/links" def click_in(view, selector, then, button=Qt.MouseButton.LeftButton): js = ("(function(){var e=document.querySelector(%r); if(!e) return null; e.scrollIntoView({block:'center'});" "var r=e.getBoundingClientRect(); return [r.x+r.width/2, r.y+r.height/2]})()" % selector) def at(xy): if not xy: stamp(f"NO {selector}"); return finish(6) zoom = view.zoomFactor(); point = QPointF(xy[0] * zoom, xy[1] * zoom) for kind, buttons in ((QEvent.Type.MouseButtonPress, button), (QEvent.Type.MouseButtonRelease, Qt.MouseButton.NoButton)): Application.sendEvent(view.focusProxy(), QMouseEvent(kind, point, QPointF(view.mapToGlobal(point)), button, buttons, Qt.KeyboardModifier.NoModifier)) QTimer.singleShot(1500, then) view.page().runJavaScript(js, at) def tabs(): return [window._tabs.widget(i).url().toString().replace(SITE, "") for i in range(window._tabs.count())] def report(label): current = window.current() stamp(f"LINK {label} tabs={len(tabs())} current={current.url().toString().replace(SITE, '')!r} " f"current_is_new={current is not links_view} worksheet={current in window._pdf_views}") def back_to_links(): window._tabs.setCurrentWidget(links_view) def go_links(then): # From the lesson the site sent us to after signing in, to the links page, in this tab. links_view.load(QUrl(f"{SITE}/links")) when(lambda: links_view.url().path() == "/links", lambda: QTimer.singleShot(1200, then), "the links page") steps = [] if variant == "follow": order = [("#blank", "text link, target=_blank, to a picture"), ("#external", "another site"), ("#linked-picture", "a linked picture"), ("#pdf", "a worksheet PDF"), ("#popup", "a lesson button that opens a window"), ("#button", "a lesson button"), ("#anchor", "an in-page anchor"), ("#same", "a link with no target")] def next_link(i=0): if i == len(order): # Back, as the right-click menu's Back does, then close every tab the links opened. links_view.page().triggerAction(links_view.page().WebAction.Back) def closed_all(): report("after Back") for view in [window._tabs.widget(j) for j in range(window._tabs.count())]: if view is not links_view: window.close_tab(window._tabs.indexOf(view)) QTimer.singleShot(800, lambda: (report("after closing the opened tabs"), stamp(f"TABS {tabs()}"), finish())) return QTimer.singleShot(2000, closed_all) selector, label = order[i] back_to_links() click_in(links_view, selector, lambda: (report(f"{selector} ({label})"), stamp(f"BUTTON {links_view.page().title()!r}") if selector == "#button" else None, QTimer.singleShot(1500 if selector == "#pdf" else 200, lambda: next_link(i + 1)))) when(on_links, lambda: go_links(lambda: (report("start"), next_link())), "signed in") elif variant in ("pdf", "image", "image-rclick", "tabs-print"): target = {"pdf": "#pdf", "image": "#blank", "image-rclick": "#blank", "tabs-print": "#blank"}[variant] def opened(): report(f"{target} opened") view = window.current() if variant == "pdf": QTimer.singleShot(2500, lambda: (QTimer.singleShot(0, lambda: window.print_view(window.current())), when(ready, lambda: (keep_preview(), press_print()), "worksheet preview"))) elif variant == "image": QTimer.singleShot(0, lambda: window.print_view(view)) when(lambda: ready() and preview()._picture is not None, lambda: ( stamp(f"PICTURE picture_box={preview()._picture_box.isVisible()} image={preview()._picture.width()}x{preview()._picture.height()}"), keep_preview(), press_print()), "picture preview") elif variant == "image-rclick": def menu_up(menu): items = [a.text() for a in menu.actions() if a.text()] stamp(f"MENU {items}") action = next(a for a in menu.actions() if a.text() == "Print image…") menu.close() when(lambda: ready() and preview()._picture is not None, lambda: ( stamp(f"PICTURE picture_box={preview()._picture_box.isVisible()} image={preview()._picture.width()}x{preview()._picture.height()}"), keep_preview(), press_print()), "picture preview") QTimer.singleShot(0, action.trigger) click_in(view, "img", lambda: when(lambda: Application.activePopupWidget() is not None, lambda: menu_up(Application.activePopupWidget()), "menu"), button=Qt.MouseButton.RightButton) else: # Three links opened, two of them closed again, then the page they came from printed. def more(): back_to_links() click_in(links_view, "#external", lambda: (back_to_links(), click_in(links_view, "#linked-picture", close_two))) def close_two(): for view in [window._tabs.widget(j) for j in range(window._tabs.count())][-2:]: window.close_tab(window._tabs.indexOf(view)) back_to_links(); report("before printing") QTimer.singleShot(0, lambda: window.print_view(links_view)) when(ready, lambda: (keep_preview(), press_print()), "page preview") more() when(on_links, lambda: go_links(lambda: (back_to_links(), click_in(links_view, target, lambda: QTimer.singleShot(1500, opened)))), "signed in") elif scenario.startswith("rclick"): # A lesson behind a sign-in, with pictures in it; a real right-click. # rclick-page on the words: the page prints # rclick-image:ID:SIZE:POSITION[:gray] on a picture: that picture prints # rclick-broken a picture that never loaded # rclick-cancel a picture, then Cancel from PyQt6.QtCore import QEvent, QPointF, Qt from PyQt6.QtGui import QMouseEvent from PyQt6.QtWidgets import QApplication as Application parts = scenario.split(":") target = {"rclick-page": "words", "rclick-page-gray": "words", "rclick-broken": "broken", "rclick-cancel": "private-jpg"}.get(parts[0], parts[1] if len(parts) > 1 else "") clipboard = Application.clipboard() view = window.open_tab(); view.load(QUrl(f"{SITE}/login")) page_ready = [] def poll_ready(): if view.url().path() == "/lesson": view.page().runJavaScript("window.__ready === true", lambda ok: page_ready.append(1) if ok else None) if not page_ready: QTimer.singleShot(200, poll_ready) QTimer.singleShot(500, poll_ready) def right_click(then): js = ("(function(){var e=document.getElementById(%r);e.scrollIntoView({block:'center'});" "var r=e.getBoundingClientRect();return [r.x+r.width/2,r.y+r.height/2]})()" % target) def at(xy): zoom = view.zoomFactor(); x, y = xy[0] * zoom, xy[1] * zoom clipboard.setText("TEACHER'S TEXT, John 3:16") stamp(f"RCLICK {target} at {x:.0f},{y:.0f} since={time.time():.3f}") for kind, buttons in ((QEvent.Type.MouseButtonPress, Qt.MouseButton.RightButton), (QEvent.Type.MouseButtonRelease, Qt.MouseButton.NoButton)): Application.sendEvent(view.focusProxy(), QMouseEvent( kind, QPointF(x, y), QPointF(view.mapToGlobal(QPointF(x, y))), Qt.MouseButton.RightButton, buttons, Qt.KeyboardModifier.NoModifier)) when(lambda: Application.activePopupWidget() is not None, lambda: then(Application.activePopupWidget()), "menu") view.page().runJavaScript(js, at) def choose(menu, text, after): items = [a.text() for a in menu.actions() if a.text()] stamp(f"MENU {items}") action = next((a for a in menu.actions() if a.text() == text), None) menu.close() if action is None: stamp(f"NO {text!r} IN THE MENU"); return finish(5) after() QTimer.singleShot(0, action.trigger) def picture_ready(): d = preview(); return d is not None and d._picture is not None and d._pdf is not None def clipboard_line(): stamp(f"CLIPBOARD {clipboard.text()!r}") if parts[0] == "rclick-page": go = lambda: right_click(lambda m: choose(m, "Print page…", lambda: when(ready, lambda: ( clipboard_line(), keep_preview(), press_print()), "page preview"))) elif parts[0] == "rclick-page-gray": # The page with all its pictures, in black and white: the slow case. def gray_page(): start = time.monotonic(); preview()._colour.setCurrentIndex(1) when(ready, lambda: ( stamp(f"GRAY PAGE took={time.monotonic() - start:.1f} shown_gray={shown_gray()} pages={preview()._pages}"), keep_preview(), press_print()), "black and white", limit=70) go = lambda: right_click(lambda m: choose(m, "Print page…", lambda: when(ready, gray_page, "page preview"))) elif parts[0] == "rclick-broken": go = lambda: right_click(lambda m: choose(m, "Print image…", lambda: when( lambda: preview() is not None and "could not be taken" in preview()._status.text(), lambda: ( stamp(f"BROKEN status={preview()._status.text()!r} print_enabled={preview()._print.isEnabled()} " f"pages={preview()._pages} picture={preview()._picture is not None}"), clipboard_line(), preview()._cancel.click(), finish()), "the picture failing", limit=15))) elif parts[0] == "rclick-cancel": go = lambda: right_click(lambda m: choose(m, "Print image…", lambda: when(picture_ready, lambda: ( stamp("PREVIEW " + describe(preview())), clipboard_line(), preview()._cancel.click(), finish()), "picture preview"))) else: _, _, size, position, *colour = parts def lay_out(): d = preview() d._size.setCurrentIndex(d._size.findData(int(size))) d._place.choose(position.replace("-", " ")) if colour == ["gray"]: d._colour.setCurrentIndex(1) QTimer.singleShot(900, lambda: when(ready, lambda: ( stamp(f"PICTURE picture_box={d._picture_box.isVisible()} image={d._picture.width()}x{d._picture.height()} " f"title={d._title!r} shown_gray={shown_gray()}"), clipboard_line(), keep_preview(), press_print()), "layout")) go = lambda: right_click(lambda m: choose(m, "Print image…", lambda: when(picture_ready, lay_out, "picture preview"))) when(lambda: bool(page_ready), go, "the lesson and its pictures", limit=30) elif scenario.startswith("picture:"): # picture:FILE:SIZE:POSITION:PAPER:FILL[:color] # Opens a picture on its own, chooses the layout with the preview's own # controls, keeps the exact PDF the preview showed, then prints it. _, name, size, position, paper, fill, *rest = scenario.split(":") view = window.open_tab(); view.load(QUrl(f"http://127.0.0.1:8089/{name}")) def ready_picture(): d = preview() return d is not None and d._picture is not None and d._pdf is not None def choose(): d = preview() d._size.setCurrentIndex(d._size.findData(int(size))) d._place.choose(position.replace("-", " ")) d._turned.setCurrentIndex(d._turned.findData(paper)) d._fill.setChecked(fill == "fill") if rest and rest[0] == "gray": d._colour.setCurrentIndex(1) # The layout settles (120 ms), then black and white if asked for. QTimer.singleShot(900, lambda: when(ready, keep_and_print, "layout")) def keep_and_print(): import shutil d = preview() out = Path("/tmp/rig-out"); out.mkdir(exist_ok=True) shutil.copy(d._pdf, out / "preview.pdf") stamp(f"PICTURE picture_box={d._picture_box.isVisible()} size_enabled={d._size.isEnabled()} " f"image={d._picture.width()}x{d._picture.height()} labels={d._colour_label.text()!r},{d._colour.itemText(0)!r} " f"shown_gray={shown_gray()}") press_print() QTimer.singleShot(2500, lambda: (QTimer.singleShot(0, lambda: window.print_view(view)), when(ready_picture, choose, "picture preview"))) elif scenario == "dark-page": # A lesson page with a black background. Chrome does not print page # backgrounds unless asked, and neither may Wonder Ink. view = window.open_tab() view.setHtml("Night sky" + "

Stars

" + "

God made the stars.

" * 40 + "", QUrl("https://app.wonderink.org/night")) def keep_dark(): import shutil out = Path("/tmp/rig-out"); out.mkdir(exist_ok=True) shutil.copy(preview()._pdf, out / "preview.pdf") press_print() view.loadFinished.connect(lambda ok: (QTimer.singleShot(0, lambda: window.print_view(view)), when(ready, keep_dark, "dark preview"))) elif scenario == "repeat": view = window.open_tab(); lesson(view); n = [0] def once(): if n[0] == 5: return finish() n[0] += 1; QTimer.singleShot(0, lambda: window.print_view(view)) when(ready, lambda: press_print(then=lambda: QTimer.singleShot(200, once)), "preview") view.loadFinished.connect(lambda ok: once() if n[0] == 0 else None) elif scenario in ("pdf", "pdf-options"): window.open_pdf(worksheet()); view = window.current() if scenario == "pdf-options": actions["pdf-options"] = lambda view: when(ready, lambda: ( preview()._which.setCurrentIndex(1), preview()._range.setText("2"), preview()._copies.setValue(3), preview()._colour.setCurrentIndex(1), when(ready, press_print, "black and white")), "preview") QTimer.singleShot(2500, lambda: (QTimer.singleShot(0, lambda: window.print_view(view)), actions[scenario if scenario == "pdf-options" else "web"](view))) elif scenario == "pdf-viewer-button": # The viewer's own print button reaches the page as a print request. window.open_pdf(worksheet()); view = window.current() QTimer.singleShot(2500, lambda: (actions["web"](view), view.page().printRequested.emit())) elif scenario == "inline": view = window.open_tab(); view.load(QUrl("http://127.0.0.1:8089/ark.pdf")) QTimer.singleShot(4000, lambda: (stamp(f"inline mime={window._mime.get(view)!r} shows_pdf={window._shows_pdf(view)}"), QTimer.singleShot(0, lambda: window.print_view(view)), when(ready, lambda: (stamp(f"worksheet tab opened: tabs={window._tabs.count()} current_is_pdf={window.current() in window._pdf_views}"), press_print()), "inline preview"))) else: if scenario == "page-closes-itself": lesson(window.open_tab(), "The other lesson") view = window.open_tab(); lesson(view) fired = [] def go(ok): if fired: return fired.append(1); QTimer.singleShot(0, lambda: window.print_view(view)); actions[scenario](view) view.loadFinished.connect(go) QTimer.singleShot(90000, lambda: (stamp("TIMEOUT"), app.exit(3))) rc = app.exec() stamp(f"EXIT {rc} scratch_left={sorted(p.name for p in Path(os.environ.get('XDG_RUNTIME_DIR','/tmp')).glob('wonderink-print-*'))}") sys.exit(rc)