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:
co-authored by
Claude Opus 5.5
parent
10433b850d
commit
48b3097c43
@@ -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
@@ -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
|
||||
|
||||
Executable
+158
@@ -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
|
||||
Reference in New Issue
Block a user