feat: a lesson in Wonder Ink is the size the browser shows it, and zooms

Teachers found lessons in Wonder Ink too small next to the ordinary
browser. Measured by asking each page for devicePixelRatio: Wonder Ink
follows the display scale exactly (dpr 1.5 at 150%), and Chrome is
scaled TWICE (dpr 2.25) — under Plasma it follows the compositor itself
and classroom-browser also forces UI_SCALE, a flag from the sway session.

The browser is left as it is. Wonder Ink's pages start at zoom =
UI_SCALE, the room's own number (1.0 at 100%), and measured afterwards
match the browser: dpr 2.25 and 853 px against 2.25 and 854. The window
is not zoomed; Qt scales it once, correctly.

Ctrl+Plus/=/keypad, Ctrl+Minus, Ctrl+0, Ctrl+wheel and the right-click
menu set one level for every tab, remembered as LEVEL= in its own file,
relative to the room. Two actions on one key are ambiguous to Qt and it
then does neither — Ctrl+Minus did nothing until each command became one
action with all its keys, which also fixes Ctrl+Shift+Tab.

The engine profile is built by one function, which the lab's checks now
use: a check with its own profile showed every worksheet blank.

The lab fences 10.10.0.0/16 as well: the development host reaches the
real Elmer over Tailscale, and the lab provisions a queue pointing at it.
Four lab checks written for older code are corrected; one had failed on
every run since 0.2.3.

tools/lab/wonderink-zoom-check: 18 checks. Lab: converged 120, print 39,
worksheets 24, all passing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-09-23 19:04:58 -05:00
co-authored by Claude Opus 5.5
parent 10433b850d
commit 48b3097c43
7 changed files with 527 additions and 62 deletions
+15 -7
View File
@@ -302,8 +302,11 @@ check "the launcher names the shell's app id" ekklesia-wonderink \
# Comments stripped first. The shell has a comment saying Ctrl+T is
# deliberately absent, and a check that cannot tell that from a binding
# is a check that fails for being right.
# One text box exists, in the print preview, for the page range. The
# check reads the code outside that class; anywhere else a text box in
# this window would be an address bar.
check "no code offers a new tab button or an address bar" 0 \
"$(in_lab "grep -vE '^[[:space:]]*#' /usr/local/bin/classroom-wonderink | grep -cE 'QLineEdit|setCornerWidget|addressBar|bind\\(\"Ctrl\\+T' || true")"
"$(in_lab "sed '/^class PrintPreview/,/^def notify/d' /usr/local/bin/classroom-wonderink | grep -vE '^[[:space:]]*#' | grep -cE 'QLineEdit\\(|setCornerWidget|addressBar|bind\\(\"Ctrl\\+T' || true")"
# Separate from the ordinary browser, which is the whole reason the split
# exists: a teacher's Wonder Ink sign-in and the room's shared YouTube
# account must never be in one profile.
@@ -329,7 +332,7 @@ check "and the browser is untouched by any of this" 0 \
section "a lesson's worksheets come back as tabs"
check "worksheets have a directory of Wonder Ink's own" 1 \
"$(in_lab 'grep -c "\"Wonder Ink\"$" /usr/local/bin/classroom-wonderink || true')"
"$(in_lab 'grep -c ") / \"Wonder Ink\"$" /usr/local/bin/classroom-wonderink || true')"
check "a finished PDF becomes a tab" 1 \
"$(in_lab 'grep -c "def open_worksheet" /usr/local/bin/classroom-wonderink || true')"
check "checked on its contents, not its name" 1 \
@@ -344,8 +347,10 @@ check "and a guard consults it on every navigation" 1 \
"$(in_lab 'grep -c "def acceptNavigationRequest" /usr/local/bin/classroom-wonderink || true')"
check "a local document cannot read other local documents" 1 \
"$(in_lab 'grep -c "LocalContentCanAccessFileUrls, False" /usr/local/bin/classroom-wonderink || true')"
check "no file chooser behind the print dialog" 1 \
"$(in_lab 'grep -c "PrintDialogOption.PrintToFile, False" /usr/local/bin/classroom-wonderink || true')"
# Qt's print dialog, and its Print to File chooser, are gone altogether:
# the preview offers CUPS printers and nothing else.
check "no file chooser behind printing" 0 \
"$(in_lab 'grep -cE "QFileDialog|QPrintDialog" /usr/local/bin/classroom-wonderink || true')"
check "nothing downloaded is handed to the desktop to run" 0 \
"$(in_lab 'grep -cE "QDesktopServices|xdg-open" /usr/local/bin/classroom-wonderink || true')"
check "and no file browser was added" 0 \
@@ -365,10 +370,13 @@ check "device.conf carries the setting" 1 \
"$(in_lab 'grep -c "^WONDERINK_ENABLED=" /etc/ekklesia-classroom/device.conf || true')"
check "and it defaults to on" true \
"$(in_lab 'sed -n "s/^WONDERINK_ENABLED=//p" /etc/ekklesia-classroom/device.conf')"
# One line the setting switches, since 0.2.3's one-setting-per-application
# layout: var WONDERINK = true|false. This asked for the older marker
# until 0.2.6.1 and so failed on every lab run in between.
check "the layout carries a marker the setting can switch" 1 \
"$(in_lab 'grep -c "CLASSROOM-WONDERINK-LAUNCHER \*/ \"applications:ekklesia-wonderink.desktop\"" /usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop/contents/layouts/org.kde.plasma.desktop-layout.js || true')"
check "and it is switched on for this machine" 0 \
"$(in_lab 'grep -c "// /\* CLASSROOM-WONDERINK-LAUNCHER" /usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop/contents/layouts/org.kde.plasma.desktop-layout.js || true')"
"$(in_lab 'grep -cE "^var WONDERINK = (true|false);" /usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop/contents/layouts/org.kde.plasma.desktop-layout.js || true')"
check "and it is switched on for this machine" 1 \
"$(in_lab 'grep -c "^var WONDERINK = true;" /usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop/contents/layouts/org.kde.plasma.desktop-layout.js || true')"
section "the room's own parts survived"
check "parent call installed" 1 "$(in_lab 'ls /usr/local/bin/classroom-parent-call 2>/dev/null | wc -l')"
+9 -4
View File
@@ -90,11 +90,16 @@ up)
# Container-local: this netns belongs to the container alone and
# nothing on the host or the church network is changed by it. Debian's
# own mirrors are untouched, so installs still work.
# 10.10 is the printers. The package provisions Elmer at
# socket://10.10.0.155:9100, and the development host reaches that
# address over Tailscale, so an unfenced lab that printed would have
# put paper in the church's real printer.
#
# A blackhole route rather than a firewall rule: the lab image has no
# iptables, and the rig's guard had been quietly doing nothing because
# of it. Routes need no netfilter modules and cannot silently fail.
docker exec "$NAME" sh -c '
for net in 10.30.0.0/16 10.40.0.0/16; do
for net in 10.10.0.0/16 10.30.0.0/16 10.40.0.0/16; do
ip route replace blackhole "$net" || exit 1
done' || echo " WARNING: could not fence the church networks"
@@ -102,7 +107,7 @@ up)
# bash feature: under dash it fails whatever the network is doing, so
# the check passed on a lab that could reach the broker perfectly
# well. Measured.
for target in 10.40.0.2/1883 10.30.0.2/1025; do
for target in 10.40.0.2/1883 10.30.0.2/1025 10.10.0.155/9100; do
if docker exec "$NAME" timeout 4 bash -c "exec 3<>/dev/tcp/${target}" 2>/dev/null; then
echo "REFUSING TO CONTINUE: ${target} is reachable from the lab" >&2
exit 1
@@ -140,11 +145,11 @@ reboot)
# named after container ids on the real broker, from sessions that
# had been fenced when they started and were not after a reboot.
docker exec "$NAME" sh -c '
for net in 10.30.0.0/16 10.40.0.0/16; do
for net in 10.10.0.0/16 10.30.0.0/16 10.40.0.0/16; do
ip route replace blackhole "$net" || exit 1
done' || echo " WARNING: could not fence the church networks"
for target in 10.40.0.2/1883 10.30.0.2/1025; do
for target in 10.40.0.2/1883 10.30.0.2/1025 10.10.0.155/9100; do
if docker exec "$NAME" timeout 4 bash -c "exec 3<>/dev/tcp/${target}" 2>/dev/null; then
echo "REFUSING TO CONTINUE: ${target} is reachable from the lab" >&2
exit 1
+158
View File
@@ -0,0 +1,158 @@
#!/bin/bash
# How large is a lesson in Wonder Ink, and do the keys change it?
#
# Runs the INSTALLED classroom-wonderink under a real QtWebEngine
# (offscreen), builds its window the way main() does, and asks the page
# itself what it is rendering at. Keys are delivered through Qt's own key
# path to the focused page — the same shortcut map a keyboard reaches —
# because the lab's injected keyboard does not reach the nested session.
# A physical Ctrl+Plus is a hardware check.
#
# tools/lab/wonderink-zoom-check
set -uo pipefail
NAME="${LAB_NAME:-classroom-v2-lab}"
COMMAND="${WONDERINK:-/usr/local/bin/classroom-wonderink}"
docker exec -i \
-e QT_QPA_PLATFORM=offscreen \
-e QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu" \
-e COMMAND="$COMMAND" \
"$NAME" bash -c 'export XDG_RUNTIME_DIR=$(mktemp -d) HOME=$(mktemp -d); python3 -' <<'PY'
import importlib.util, os, sys
from importlib.machinery import SourceFileLoader
loader = SourceFileLoader("wonderink", os.environ["COMMAND"])
spec = importlib.util.spec_from_loader("wonderink", loader)
ink = importlib.util.module_from_spec(spec)
loader.exec_module(ink)
from PyQt6.QtCore import QEventLoop, Qt, QTimer
from PyQt6.QtTest import QTest
from PyQt6.QtWebEngineCore import QWebEngineProfile
from PyQt6.QtWidgets import QApplication
app = QApplication(sys.argv[:1])
passed = failed = 0
def check(label, condition, detail=""):
global passed, failed
if condition:
print(f" \033[32mPASS\033[0m {label}")
passed += 1
else:
print(f" \033[31mFAIL\033[0m {label}" + (f" [{detail}]" if detail else ""))
failed += 1
def wait(ms):
loop = QEventLoop()
QTimer.singleShot(ms, loop.quit)
loop.exec()
def page_dpr(view):
answer = []
view.page().runJavaScript("window.devicePixelRatio", answer.append)
for _ in range(50):
if answer:
return answer[0]
wait(100)
return None
PAGE = "data:text/html,<title>lesson</title><p>Noah built an ark.</p>"
KEY = {
"Ctrl+=": (Qt.Key.Key_Equal, Qt.KeyboardModifier.ControlModifier),
"Ctrl+Plus": (Qt.Key.Key_Plus, Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier),
"keypad Ctrl+Plus": (Qt.Key.Key_Plus, Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.KeypadModifier),
"Ctrl+Minus": (Qt.Key.Key_Minus, Qt.KeyboardModifier.ControlModifier),
"keypad Ctrl+Minus": (Qt.Key.Key_Minus, Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.KeypadModifier),
"Ctrl+0": (Qt.Key.Key_0, Qt.KeyboardModifier.ControlModifier),
"Ctrl+Shift+Tab": (Qt.Key.Key_Backtab, Qt.KeyboardModifier.ControlModifier | Qt.KeyboardModifier.ShiftModifier),
}
def press(window, name):
target = QApplication.focusWidget() or window.current()
QTest.keyClick(target, *KEY[name])
wait(400)
def open_window(room_zoom):
profile = QWebEngineProfile(f"zoomcheck{room_zoom}", app)
window = ink.WonderInk(profile, room_zoom=room_zoom)
window.resize(1280, 800)
window.show()
for _ in range(3):
window.open_tab(PAGE)
wait(2500)
window.current().setFocus()
wait(300)
return window
def factors(window):
return [round(window._tabs.widget(i).zoomFactor(), 3) for i in range(window._tabs.count())]
print("1. a room's normal size is the browser's")
for room in (1.0, 1.5, 2.0):
ink.SETTINGS.unlink(missing_ok=True)
window = open_window(room)
check(f"UI_SCALE {room}: every tab starts at {room}", factors(window) == [room] * 3, factors(window))
window.close()
wait(300)
print()
print("2. the keys a teacher knows, at UI_SCALE 1.5")
ink.SETTINGS.unlink(missing_ok=True)
window = open_window(1.5)
for name, level in (("Ctrl+=", 1.1), ("Ctrl+Plus", 1.25), ("keypad Ctrl+Plus", 1.5),
("Ctrl+Minus", 1.25), ("keypad Ctrl+Minus", 1.1), ("Ctrl+0", 1.0)):
press(window, name)
check(f"{name} makes it {round(level * 100)}% in every tab",
window.level() == level and factors(window) == [round(1.5 * level, 3)] * 3,
f"level={window.level()} factors={factors(window)}")
press(window, "Ctrl+Plus")
dpr = page_dpr(window.current())
check("and the page itself renders at it", dpr is not None and abs(dpr - 1.65) < 0.01, dpr)
print()
print("3. new tabs, and the next time Wonder Ink opens")
view = window.open_tab(PAGE)
wait(1500)
check("a new tab starts at the teacher's size", round(view.zoomFactor(), 3) == 1.65, view.zoomFactor())
check("the level is saved on its own", ink.SETTINGS.read_text().strip() == "LEVEL=1.1",
ink.SETTINGS.read_text())
check("in its own file, not the web profile",
"wonderink-shell" not in str(ink.SETTINGS) and ink.SETTINGS.name == "wonderink.conf")
index = window._tabs.currentIndex()
press(window, "Ctrl+Shift+Tab")
check("Ctrl+Shift+Tab still goes back a tab", window._tabs.currentIndex() == index - 1,
f"{index} -> {window._tabs.currentIndex()}")
window.close()
wait(300)
window = open_window(2.0)
check("reopened in a room at 2.0, the teacher's 110% is kept",
window.level() == 1.1 and factors(window) == [2.2] * 3, factors(window))
window.close()
ink.SETTINGS.write_text("LEVEL=banana\n")
check("a damaged setting is the normal size", ink.load_level() == 1.0)
ink.SETTINGS.write_text("LEVEL=40\n")
check("an absurd one too", ink.load_level() == 1.0)
window = open_window(3.0)
press(window, "keypad Ctrl+Plus"); press(window, "keypad Ctrl+Plus"); press(window, "keypad Ctrl+Plus")
check("a large room zoomed further never passes the engine's limit",
max(factors(window)) <= 5.0, factors(window))
window.close()
print()
print(f"{passed} passed, {failed} failed")
sys.exit(1 if failed else 0)
PY