tools/lab/wonderink-workspace-check launches the installed command again and again against a stand-in for WonderInk's start page whose sign-in is Basic auth, as WonderInk's is: restored after 5 and 90 minutes; fresh after two hours and a minute, a day and a week; reopened untouched until it lapses; a click after a restore counts and the restore does not; a busy page and an untouched print preview do not; Print does; closed, reordered, worksheet and missing-worksheet tabs; killed outright; a restore that crashes twice; corrupt, future-format and half-written files; the file holding no credential; restored tabs printing. 59 checks. The print check follows every kind of link on a signed-in page with one click — new tab in front for links and window.open, nothing for buttons and anchors, same-tab for no target, Back, closing — and prints a PDF and a picture reached by a link, and a page after several tabs. 0.2.6.3's scenarios are unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]>
202 lines
7.4 KiB
Python
202 lines
7.4 KiB
Python
"""One launch of Wonder Ink, its recent tabs restored or not, and a few steps.
|
||
|
||
usage: wonderink-workspace-rig.py WONDERINK_COMMAND SITE STEP...
|
||
|
||
Run by tools/lab/wonderink-workspace-check inside the lab, against the
|
||
command the package installed, started as main() starts it: the real
|
||
profile, downloads connected, then open_workspace() — which restores the
|
||
recent tabs or opens the start page. The start page is the lab's
|
||
stand-in for WonderInk's sign-in (tools/lab/wonderink-lesson-site.py,
|
||
/app) rather than the real site.
|
||
|
||
Steps, in order:
|
||
|
||
open:PATH open SITE+PATH (or a full http address) in a new tab, in front
|
||
worksheet:NAME a worksheet PDF downloaded and opened in a tab
|
||
signin click the stand-in's Sign In (a real click)
|
||
touch one real click on the page in front
|
||
front:N bring tab N to the front, as clicking it does (a real click)
|
||
close:N close tab N, as its × does
|
||
move:A:B drag tab A to where B is
|
||
preview:N the print preview open N seconds, nobody touching it
|
||
print Print, and the preview's Print button pressed by a real click
|
||
wait:N N seconds, nobody touching anything
|
||
state what the tabs are, and what the kept file says
|
||
quit close the window
|
||
crash SIGKILL, no chance to write anything
|
||
"""
|
||
|
||
import importlib.machinery
|
||
import importlib.util
|
||
import json
|
||
import os
|
||
import signal
|
||
import sys
|
||
import time
|
||
from pathlib import Path
|
||
|
||
loader = importlib.machinery.SourceFileLoader("wonderink", sys.argv[1])
|
||
spec = importlib.util.spec_from_loader("wonderink", loader)
|
||
wi = importlib.util.module_from_spec(spec)
|
||
loader.exec_module(wi)
|
||
|
||
from PyQt6.QtCore import QEvent, QEventLoop, QPointF, Qt, QTimer # noqa: E402
|
||
from PyQt6.QtGui import QMouseEvent, QPageSize, QPainter, QPdfWriter # noqa: E402
|
||
from PyQt6.QtWidgets import QApplication # noqa: E402
|
||
|
||
site, steps = sys.argv[2].rstrip("/"), sys.argv[3:]
|
||
wi.HOME_PAGE = f"{site}/app"
|
||
wi.notify = lambda summary, body: print("NOTIFY", summary, "|", body, flush=True)
|
||
|
||
app = QApplication(sys.argv[:1])
|
||
t0 = time.monotonic()
|
||
|
||
|
||
def stamp(message):
|
||
print(f"[{time.monotonic() - t0:6.2f}s] {message}", flush=True)
|
||
|
||
|
||
profile = wi.build_profile(app, "workspace-rig")
|
||
wi.DOWNLOADS.mkdir(parents=True, exist_ok=True)
|
||
window = wi.WonderInk(profile)
|
||
profile.downloadRequested.connect(window.on_download)
|
||
keeper = wi.open_workspace(window)
|
||
window.resize(1100, 700)
|
||
window.show()
|
||
stamp(f"START tabs={window._tabs.count()} attempts={keeper._attempts}")
|
||
|
||
|
||
def spin(seconds, until=None):
|
||
end = time.monotonic() + seconds
|
||
while time.monotonic() < end:
|
||
app.processEvents(QEventLoop.ProcessEventsFlag.AllEvents, 50)
|
||
if until is not None and until():
|
||
return True
|
||
time.sleep(0.01)
|
||
return False
|
||
|
||
|
||
def js(code, view=None):
|
||
box = []
|
||
(view or window.current()).page().runJavaScript(code, lambda result: box.append(result))
|
||
spin(5, lambda: bool(box))
|
||
return box[0] if box else None
|
||
|
||
|
||
def press(widget, point, button=Qt.MouseButton.LeftButton):
|
||
for kind, buttons in ((QEvent.Type.MouseButtonPress, button),
|
||
(QEvent.Type.MouseButtonRelease, Qt.MouseButton.NoButton)):
|
||
QApplication.sendEvent(widget, QMouseEvent(kind, point, QPointF(widget.mapToGlobal(point)),
|
||
button, buttons, Qt.KeyboardModifier.NoModifier))
|
||
|
||
|
||
def click_page(selector="body"):
|
||
view = window.current()
|
||
where = js("(function(){var e=document.querySelector(%s); if(!e) return [20, 20]; var r=e.getBoundingClientRect();"
|
||
"return [r.x+Math.min(r.width/2, 40), r.y+Math.min(r.height/2, 10)]})()" % json.dumps(selector)) or [20, 20]
|
||
zoom = view.zoomFactor()
|
||
press(view.focusProxy(), QPointF(where[0] * zoom, where[1] * zoom))
|
||
|
||
|
||
def where_of(view):
|
||
# A restored tab not yet shown is the tab it will load when it is.
|
||
if view in window._pending:
|
||
kept = keeper._pending_tabs[view]
|
||
if kept["kind"] == "worksheet":
|
||
return "worksheet:" + Path(kept["where"]).name
|
||
return kept["where"].replace(site, "")
|
||
if view in window._pdf_views:
|
||
return "worksheet:" + window._pdf_views[view].name
|
||
return view.url().toString().replace(site, "")
|
||
|
||
|
||
def state(label="STATE"):
|
||
tabs = [where_of(window._tabs.widget(i)) for i in range(window._tabs.count())]
|
||
# What the stand-in's start page says in each tab that shows it.
|
||
pages = [js("document.getElementById('state') ? document.getElementById('state').textContent : ''",
|
||
window._tabs.widget(i)) for i in range(window._tabs.count())]
|
||
kept = "none"
|
||
if wi.WORKSPACE_FILE.exists():
|
||
data = json.loads(wi.WORKSPACE_FILE.read_text())
|
||
kept = (f"tabs={len(data['tabs'])} active={data['active']} attempts={data['attempts']} "
|
||
f"age={time.time() - data['last_activity']:.0f}")
|
||
stamp(f"{label} tabs={tabs} active={window._tabs.currentIndex()} kept[{kept}] pages={[p for p in pages if p]} "
|
||
f"unshown={len(window._pending)}")
|
||
|
||
|
||
def make_worksheet(name):
|
||
path = wi.DOWNLOADS / f"{name}.pdf"
|
||
writer = QPdfWriter(str(path))
|
||
writer.setPageSize(QPageSize(QPageSize.PageSizeId.Letter))
|
||
painter = QPainter(writer)
|
||
painter.drawText(200, 400, f"WORKSHEET {name}")
|
||
painter.end()
|
||
return path
|
||
|
||
|
||
def run():
|
||
spin(3)
|
||
|
||
for step in steps:
|
||
name, _, rest = step.partition(":")
|
||
stamp(f"STEP {step}")
|
||
|
||
if name == "open":
|
||
window.open_tab(rest if rest.startswith("http") else site + rest)
|
||
spin(2.5)
|
||
elif name == "worksheet":
|
||
window.open_pdf(make_worksheet(rest))
|
||
spin(2.5)
|
||
elif name == "signin":
|
||
click_page("#signin")
|
||
spin(1.5)
|
||
elif name == "touch":
|
||
click_page()
|
||
spin(1.5)
|
||
elif name == "front":
|
||
bar = window._tabs.tabBar()
|
||
press(bar, QPointF(bar.tabRect(int(rest)).center()))
|
||
spin(1.5)
|
||
elif name == "close":
|
||
window.close_tab(int(rest))
|
||
spin(1.5)
|
||
elif name == "move":
|
||
a, b = (int(n) for n in rest.split(":"))
|
||
window._tabs.tabBar().moveTab(a, b)
|
||
spin(1.5)
|
||
elif name == "preview":
|
||
QTimer.singleShot(int(float(rest) * 1000), lambda: window._preview.reject() if window._preview else None)
|
||
window.print_view(window.current())
|
||
spin(1.5)
|
||
elif name == "print":
|
||
def pressed():
|
||
preview = window._preview
|
||
if preview is None or preview._pdf is None:
|
||
return QTimer.singleShot(200, pressed)
|
||
button = preview._print
|
||
press(button, QPointF(button.rect().center()))
|
||
QTimer.singleShot(500, pressed)
|
||
window.print_view(window.current())
|
||
spin(2)
|
||
stamp(f"PRINTED preview_open={window._preview is not None}")
|
||
elif name == "wait":
|
||
spin(float(rest))
|
||
elif name == "state":
|
||
state()
|
||
elif name == "quit":
|
||
window.close()
|
||
spin(1)
|
||
state("QUIT")
|
||
app.quit()
|
||
return
|
||
elif name == "crash":
|
||
stamp("CRASH")
|
||
os.kill(os.getpid(), signal.SIGKILL)
|
||
|
||
app.quit()
|
||
|
||
|
||
QTimer.singleShot(0, run)
|
||
QTimer.singleShot(240_000, lambda: (stamp("TIMEOUT"), app.exit(3)))
|
||
sys.exit(app.exec())
|