Remove CEF: the web content crate, ElyMark's page, their packaging, licences and tests (C14.5)

PRD and agents.md name the native route; ElyMail's body will not use
CEF or Electron. ElyMark's package carries the KaTeX fonts' OFL.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-12 03:15:30 -04:00
1 parent 2e8379b0b1
commit 8a942e6d80
77 files changed
+105 -10718

No files matched your search

+3 -11
View File
@@ -10,17 +10,7 @@ fail() {
exit 1
}
check() { python3 "$root/tests/scripts/comments.py" "$work" >/dev/null 2>&1; }
mkdir -p "$work/apps/a/web/src" "$work/tests/scripts"
printf '// One short line.\nconst a = 1\n' >"$work/apps/a/web/src/a.js"
check || fail "a short comment failed"
printf '/* A comment\n spanning lines. */\n' >"$work/apps/a/web/src/a.js"
! check || fail "a multi-line JS block comment passed"
printf 'const a = 1 /* A comment\n spanning lines. */\n' >"$work/apps/a/web/src/a.js"
! check || fail "an inline multi-line JS block comment passed"
printf '// One two three four five six seven eight nine ten eleven.\n' >"$work/apps/a/web/src/a.js"
! check || fail "an eleven-word JS comment passed"
rm "$work/apps/a/web/src/a.js"
mkdir -p "$work/site/src"
mkdir -p "$work/site/src" "$work/tests/scripts"
for ext in ts css; do
printf '/* A comment\n spanning lines. */\n' >"$work/site/src/a.$ext"
! check || fail "a multi-line .$ext block comment passed"
@@ -30,6 +20,8 @@ for ext in ts css; do
check || fail "a short .$ext comment failed"
rm "$work/site/src/a.$ext"
done
printf 'const a = 1 /* A comment\n spanning lines. */\n' >"$work/site/src/a.ts"
! check || fail "an inline multi-line .ts block comment passed"
printf 'const glob = "/*.css";\n' >"$work/site/src/a.ts"
check || fail "a /* inside a .ts string failed"
printf 'const a = 1; // One two three four five six seven eight nine ten eleven.\n' >"$work/site/src/a.ts"
+1 -2
View File
@@ -20,7 +20,6 @@ PATTERNS = [
"crates/**/*.h",
"patches/**/*.patch",
"tests/**/*.swift",
"apps/*/web/src/**/*.js",
"site/src/**/*.ts",
"site/src/**/*.css",
"site/test/**/*.ts",
@@ -37,7 +36,7 @@ LINE_COMMENT = {
}
# Scanned by tokens: strings skipped, trailing comments found.
SCANNED = {".js": True, ".ts": True, ".css": False}
SCANNED = {".ts": True, ".css": False}
def scan(text, slashes):
-36
View File
@@ -1,36 +0,0 @@
#!/usr/bin/env bash
# ElyMark signed with hardened runtime still draws its page.
set -euo pipefail
root=$(cd "$(dirname "$0")/../.." && pwd)
[ "$(uname)" = Darwin ] || {
echo "SKIP hardened-macos: macOS only"
exit 0
}
[ -n "${ELY_SIGN_IDENTITY:-}" ] || {
echo "SKIP hardened-macos: set ELY_SIGN_IDENTITY to a signing identity"
exit 0
}
work=$(cd "$(mktemp -d)" && pwd -P)
trap 'pkill -f "$work/ElyMark.app/Contents/MacOS/" 2>/dev/null || true; sleep 1; rm -rf "$work"' EXIT
fail() {
echo "FAIL: $*" >&2
exit 1
}
ELY_DIST=$work "$root/packaging/macos/bundle.sh" ely-mark debug >"$work/bundle.log" 2>&1 ||
fail "bundle: $(tail -n 3 "$work/bundle.log")"
app=$work/ElyMark.app
codesign --verify --deep --strict "$app" || fail "the signature does not verify"
codesign -d --entitlements - "$app/Contents/Frameworks/ely-mark Helper (Renderer).app" 2>/dev/null |
grep -q allow-jit || fail "the renderer helper has no JIT entitlement"
printf '# Hardened\n' >"$work/t.md"
open -n -a "$app" --env "HOME=$work/home" --env ELY_LOG=info --env ELY_NO_DIALOG=1 "$work/t.md"
log="$work/home/Library/Application Support/ElyOffice/ely-mark/ely.log"
for _ in $(seq 240); do
grep -q 'mark: shown as' "$log" 2>/dev/null && {
echo "hardened-macos: the signed page draws"
exit 0
}
sleep 0.25
done
fail "the page never showed: $(grep -E 'web:|mark:' "$log" 2>/dev/null | tail -n 5)"
+1 -1
View File
@@ -31,7 +31,7 @@ docker build -q -t ely-smoke-fedora - >/dev/null <<'EOF'
FROM fedora:44@sha256:43b29f65a41eb9c35e1cd5323e3bdf3b655c2357a9f4f1ff2f9c2798e5045d80
RUN dnf install -y -q xorg-x11-server-Xvfb mesa-vulkan-drivers xwd xwininfo && dnf clean all
EOF
# As the work dir's owner: Git and CEF refuse root.
# As the work dir's owner: Git refuses others' repos.
inside='set -euo pipefail
slug=$1 work=$2 file=$3
shift 3
+2 -2
View File
@@ -17,7 +17,7 @@ done
release=$root/target/ubuntu/release
lo=$root/engines/lo-src/instdir/program
required=("$release"/ely-{doc,table,show,pdf,tree,mark,lok-worker,pdf-worker} "$lo/libmergedlo.so"
"$root/engines/git/bin/git" "$root/engines/cef/libcef.so")
"$root/engines/git/bin/git")
for file in "${required[@]}"; do
[ -f "$file" ] || fail "no $file; build in packaging/linux/container.sh"
magic=$(head -c 4 "$file" | od -An -c | tr -d ' ') || fail "cannot read $file"
@@ -26,7 +26,7 @@ done
list=$(mktemp)
trap 'rm -f "$list"' EXIT
find -L "$release"/ely-{doc,table,show,pdf,tree,mark,lok-worker,pdf-worker} "$lo" "$root/engines/git" \
"$root/engines/cef" -type f -print0 >"$list" || fail "cannot list every file"
-type f -print0 >"$list" || fail "cannot list every file"
checked=0
while IFS= read -r -d '' file; do
magic=$(head -c 4 "$file" | od -An -c | tr -d ' ') || fail "cannot read $file"
-105
View File
@@ -1,105 +0,0 @@
#!/usr/bin/env bash
# Idle on Linux: ElyMark rereads no theme, settings or file.
set -euo pipefail
root=$(cd "$(dirname "$0")/../.." && pwd)
fail() {
echo "FAIL: $*" >&2
exit 1
}
[ "$(uname)" = Linux ] || {
echo "SKIP: inotify is Linux"
exit 0
}
[ -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ] || {
echo "SKIP: no display"
exit 0
}
for tool in inotifywait jq hyprctl grim magick tesseract; do
command -v "$tool" >/dev/null || {
echo "SKIP: no $tool"
exit 0
}
done
[ -f "$root/engines/cef/libcef.so" ] || fail "no engines/cef; run scripts/fetch-cef.sh"
[ -f "$root/engines/content/ely-mark/index.html" ] || fail "no ElyMark page; run scripts/build-mark-content.sh"
app=$(cargo build -q --locked --message-format=json --manifest-path "$root/Cargo.toml" -p ely-mark --bin ely-mark |
jq -r 'select(.reason == "compiler-artifact" and .target.name == "ely-mark") | .executable // empty')
[ -x "$app" ] || fail "Cargo reported no ElyMark binary"
work=$(mktemp -d)
pid=
spy=
trap '[ -z "$spy" ] || kill "$spy" 2>/dev/null; [ -z "$pid" ] || stop_app "$pid"; rm -rf "$work"' EXIT
# shellcheck source=tests/scripts/linux-common.sh
. "$root/tests/scripts/linux-common.sh"
current=$work/omarchy/current
mkdir -p "$current/theme" "$work/data/ElyOffice" "$work/notes"
cp "$root/crates/ely-ui/tests/fixtures/omarchy/catppuccin.toml" "$current/theme/colors.toml"
echo catppuccin >"$current/theme.name"
echo '{"version": 1}' >"$work/data/ElyOffice/settings.json"
note=$work/notes/note.md
printf '# Idle\n\nNothing moves.\n' >"$note"
LD_LIBRARY_PATH="$root/engines/cef" ELY_OMARCHY_DIR="$current" ELY_SMOKE=hold ELY_LOG=info HOME="$work" \
XDG_DATA_HOME="$work/data" setsid "$app" "$note" 2>"$work/app.err" &
pid=$!
seen() { grep -c "$1" "$work/app.err" || true; }
logged() { # pattern, what, count before
local before=${3:-0}
for _ in $(seq 120); do
[ "$(seen "$1")" -gt "$before" ] && return
kill -0 "$pid" 2>/dev/null || fail "ElyMark quit: $(tail -n 5 "$work/app.err")"
sleep 0.25
done
fail "ElyMark missed $2"
}
logged 'mark: watching 1 folders' "its file's folder"
logged 'omarchy catppuccin applied' "the Omarchy theme"
sleep 3
# Four quiet seconds: nothing opens the theme, settings or note.
idle() { # when
inotifywait -q -m -e open --format '%w%f' "$current" "$current/theme" "$work/data/ElyOffice" "$work/notes" \
>"$work/opens" 2>/dev/null &
spy=$!
for _ in 1 2 3 4 5; do
sleep 1
kill -0 "$pid" 2>/dev/null || fail "ElyMark quit while $1: $(tail -n 5 "$work/app.err")"
done
kill "$spy"
spy=
local opens
opens=$(grep -c -E '(colors\.toml|theme\.name|settings\.json|note\.md)$' "$work/opens" || true)
[ "$opens" -eq 0 ] || fail "ElyMark $1 opened files $opens times: $(sort "$work/opens" | uniq -c)"
echo "linux: ElyMark $1 reads nothing"
}
idle "idle after start"
mkdir "$current/next-theme"
cp "$root/crates/ely-ui/tests/fixtures/omarchy/gruvbox.toml" "$current/next-theme/colors.toml"
rm -rf "$current/theme"
mv "$current/next-theme" "$current/theme"
echo gruvbox >"$current/theme.name"
logged 'omarchy gruvbox applied' "Omarchy's theme swap"
echo '{"version": 1, "author": "Tester"}' >"$work/data/ElyOffice/settings.json"
logged 'settings: changed on disk' "a settings write"
changed=$(seen 'changed outside')
printf '# Idle\n\nSomething moved.\n' >"$note"
logged 'changed outside' "an outside edit" "$changed"
# The window shows the new words.
shown=
for _ in $(seq 20); do
kill -0 "$pid" 2>/dev/null || fail "ElyMark quit: $(tail -n 5 "$work/app.err")"
id=$(hyprctl clients -j | jq -r --argjson pid "$pid" '[.[] | select(.pid == $pid and .mapped)][0].stableId // empty')
if [ -n "$id" ]; then
grim -T "$id" "$work/shot.png" 2>"$work/grim.err" || fail "grim failed: $(cat "$work/grim.err")"
magick "$work/shot.png" -colorspace Gray -negate "$work/ocr.png"
text=$(tesseract "$work/ocr.png" - 2>"$work/ocr.err") || fail "tesseract failed: $(cat "$work/ocr.err")"
grep -q 'moved' <<<"$text" && shown=1 && break
fi
sleep 0.5
done
[ -n "$shown" ] || fail "ElyMark does not show the outside edit"
sleep 2
idle "idle after changes"
+2 -3
View File
@@ -4,7 +4,7 @@ set -euo pipefail
root=$(cd "$(dirname "$0")/../.." && pwd)
script=$root/scripts/fetch-references.sh
names="Ely-GPUI-Components zed libreoffice-core mupdf git tesseract mailcore2 libetpan cef cef-rs milkdown codemirror-dev marktext colamd magpie agent-client-protocol markion markdown-rs omarchy"
names="Ely-GPUI-Components zed libreoffice-core mupdf git tesseract mailcore2 libetpan marktext colamd magpie agent-client-protocol markion markdown-rs omarchy"
fail() {
echo "FAIL: $*" >&2
@@ -29,8 +29,7 @@ for header in \
tesseract/include/tesseract/capi.h \
mailcore2/CMakeLists.txt \
libetpan/src/low-level/gmail/mailgmail.h \
libetpan/src/low-level/activesync/mailactivesync.h \
cef-rs/Cargo.toml; do
libetpan/src/low-level/activesync/mailactivesync.h; do
[ -s "$root/reference/$header" ] || fail "missing reference/$header"
done
+3 -8
View File
@@ -72,7 +72,7 @@ while IFS=$'\t' read -r slug name; do
ely-doc | ely-table | ely-show) engines=(LibreOffice) ;;
ely-pdf) engines=(MuPDF MuPDF/fonts/urw MuPDF/fonts/sil MuPDF/fonts/noto MuPDF/fonts/droid MuPDF/fonts/han OpenSSL tessdata GreatVibes) ;;
ely-tree) engines=(Git git-lfs) ;;
ely-mark) engines=(CEF content) ;;
ely-mark) engines=(KaTeX) ;;
esac
for part in LICENSE rust-crates.txt "${engines[@]}"; do
[ -n "$(find "$licenses/$part" -type f -size +0 2>/dev/null)" ] || fail "$name lacks the licences of $part"
@@ -83,9 +83,8 @@ while IFS=$'\t' read -r slug name; do
[ -n "$(find "$family" -type f -size +0 \( -iname 'licen[cs]e*' -o -iname 'copying*' -o -iname 'notice*' -o -iname 'ofl*' \))" ] ||
fail "$name lacks the licence of the ${family##*/} fonts"
done
# Nothing inside registers with macOS but CEF's helpers.
extra=$(find "$app/Contents" \( -name '*.app' -o -name '*.appex' -o -name '*.mdimporter' -o -name '*.qlgenerator' \) |
grep -v "/Frameworks/$slug Helper" || true)
# Nothing inside registers with macOS.
extra=$(find "$app/Contents" \( -name '*.app' -o -name '*.appex' -o -name '*.mdimporter' -o -name '*.qlgenerator' \))
[ -z "$extra" ] || fail "$name carries bundles macOS registers: $extra"
# No binary may need a newer macOS than the app.
least=$(/usr/libexec/PlistBuddy -c "Print :LSMinimumSystemVersion" "$app/Contents/Info.plist")
@@ -125,10 +124,6 @@ while IFS=$'\t' read -r slug name; do
# Built-ins stay links to one binary, not copies.
[ "$(du -sm "$app/Contents/Resources/git" | cut -f1)" -lt 100 ] || fail "ElyTree's Git copies its built-ins"
;;
ely-mark)
grep -qF "web: CEF loaded from $app/Contents/Frameworks" "$log" || fail "ElyMark used another CEF"
grep -qF "mark: page from $app/Contents/Resources/content" "$log" || fail "ElyMark used another page"
;;
esac
# A running app takes Finder's next file.
open -a "$app" "$files/$second"
+21 -21
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Storage: sizes shown; log cleared now, cache at restart.
# Storage: sizes shown; log cleared now, copies at restart.
set -euo pipefail
root=$(cd "$(dirname "$0")/../.." && pwd)
@@ -48,18 +48,18 @@ quit() {
for _ in $(seq 40); do kill -0 "$pid" 2>/dev/null || break; sleep 0.25; done
wait "$pid" 2>/dev/null || true
}
mkdir -p "$data/web"
# A known 2 MiB in the cache for the row.
dd if=/dev/zero of="$data/web/ely-sample" bs=1024 count=2048 2>/dev/null
mkdir -p "$data/recovery"
# A known 2 MiB in the copies for the row.
dd if=/dev/zero of="$data/recovery/ely-sample" bs=1024 count=2048 2>/dev/null
# A log long enough that clearing it shows.
head -c 8192 /dev/zero | tr '\0' 'x' >"$data/ely.log"
start
keys 'keystroke "," using command down'
seen "title=Storage" || fail "no settings window"
"$work/ax-press" "$pid" Storage >/dev/null
seen "title=Web content cache" || fail "the storage page lists no cache"
shown=$(kb "Web content cache")
[ "${shown:-0}" -ge 2048 ] || fail "the cache shows ${shown:-nothing} KB, not at least 2048"
seen "title=Recovery copies" || fail "the storage page lists no copies"
shown=$(kb "Recovery copies")
[ "${shown:-0}" -ge 2048 ] || fail "the copies show ${shown:-nothing} KB, not at least 2048"
shown=$(kb "This app's log")
[ "${shown:-0}" -ge 8 ] || fail "the log shows ${shown:-nothing} KB, not at least 8"
# Each clear asks first; nothing goes before the yes.
@@ -70,11 +70,11 @@ seen "title=Delete" || fail "clearing the log did not ask"
sleep 1
[ "$(wc -c <"$data/ely.log")" -lt 4096 ] || fail "the log was not cleared"
"$work/ax-press" "$pid" "Clear at next start" AXButton >/dev/null
seen "title=Delete" || fail "clearing the cache did not ask"
[ ! -e "$data/.clear-web" ] || fail "the cache was marked before the yes"
seen "title=Delete" || fail "clearing the copies did not ask"
[ ! -e "$data/.clear-recovery" ] || fail "the copies were marked before the yes"
"$work/ax-press" "$pid" Delete AXButton >/dev/null
seen "Clears at next start" || fail "the cache clear is not shown as pending"
[ -e "$data/ely-sample" ] || [ -d "$data/web" ] || fail "the cache went while in use"
seen "Clears at next start" || fail "the copies clear is not shown as pending"
[ -e "$data/recovery/ely-sample" ] || fail "the copies went while in use"
# Ten points kept per document, once twelve were made.
"$work/ax-press" "$pid" 10 >/dev/null
for _ in $(seq 20); do grep -q '"keep": 10' "$data/settings.json" 2>/dev/null && break; sleep 0.25; done
@@ -82,8 +82,9 @@ grep -q '"keep": 10' "$data/settings.json" || fail "the recovery limit is not ke
osascript -e "tell application \"System Events\" to click button 1 of (first window of (first process whose unix id is $pid) whose name is \"Settings\")" >/dev/null
sleep 1
# An untitled document: never autosaved, so it keeps recovery points.
tabs=$(grep -c "as tab" "$data/ely.log" || true)
keys 'keystroke "n" using command down'
for _ in $(seq 40); do grep -q "mark: shown" "$data/ely.log" && break; sleep 0.25; done
for _ in $(seq 40); do [ "$(grep -c "as tab" "$data/ely.log")" -gt "$tabs" ] && break; sleep 0.25; done
sleep 2
# Twelve points written, one edit each.
written() { grep -c "recovery: a point at" "$data/ely.log" || true; }
@@ -146,22 +147,21 @@ grep -q '"keep": 20' "$data/settings.json" || fail "the retried limit is not kep
sleep 0.5
"$work/ax-values" "$pid" | grep -q "Not done" && fail "the failure stays after a good save"
quit
touch "$data/web/ely-marker"
touch "$data/recovery/ely-marker"
start
[ ! -e "$data/web/ely-marker" ] || fail "the cache was not cleared at the start"
[ ! -e "$data/recovery/ely-marker" ] || fail "the copies were not cleared at the start"
quit
# A failed clear stops the start; the mark stays.
mkdir -p "$data/web/locked"
touch "$data/web/locked/kept" "$data/.clear-web"
chmod 555 "$data/web/locked"
mkdir -p "$data/recovery/locked"
touch "$data/recovery/locked/kept" "$data/.clear-recovery"
chmod 555 "$data/recovery/locked"
status=0
ELY_SMOKE=hold ELY_LOCALE=en ELY_NO_DIALOG=1 HOME="$home" perl -e 'alarm 60; exec @ARGV' "$root/target/debug/ely-mark" >"$work/out" 2>&1 || status=$?
chmod 755 "$data/web/locked"
chmod 755 "$data/recovery/locked"
[ "$status" = 1 ] || fail "a failed clear started anyway ($status)"
grep -q "could not be deleted" "$work/out" || fail "the failed clear is not told: $(tail -3 "$work/out")"
grep -q "web: CEF" "$work/out" && fail "the cache opened after a failed clear"
[ -e "$data/.clear-web" ] && [ -e "$data/web/locked/kept" ] || fail "the failed clear lost its mark or data"
[ -e "$data/.clear-recovery" ] && [ -e "$data/recovery/locked/kept" ] || fail "the failed clear lost its mark or data"
start
[ ! -e "$data/web/locked/kept" ] || fail "the clear was not tried again"
[ ! -e "$data/recovery/locked/kept" ] || fail "the clear was not tried again"
quit
echo "ok: storage"
-77
View File
@@ -1,77 +0,0 @@
#!/usr/bin/env bash
# A live WebView: input, fence, failures, release, paint.
set -euo pipefail
root=$(cd "$(dirname "$0")/../.." && pwd)
[ "$(uname)" = Darwin ] || { echo "SKIP: macOS only"; exit 0; }
[ -d "${ELY_CEF:-$root/engines/cef}/Chromium Embedded Framework.framework" ] || {
echo "SKIP: no CEF; run scripts/fetch-cef.sh"
exit 0
}
work=$(mktemp -d)
probe=
trap '[ -n "$probe" ] && kill "$probe" 2>/dev/null; rm -rf "$work"' EXIT
fail() {
echo "FAIL: $*" >&2
exit 1
}
cargo build -q -p ely-web-content --example webprobe --manifest-path "$root/Cargo.toml"
swiftc -O -o "$work/share" "$root/tests/scripts/window-share.swift"
swiftc -O -o "$work/press" "$root/tests/scripts/ax-press.swift"
ELY_LOG=info ELY_LOCALE=en ELY_SMOKE=hold HOME="$work" XDG_DATA_HOME="$work/data" \
perl -e 'alarm 150; exec @ARGV' "$root/target/debug/examples/webprobe" >"$work/out" 2>"$work/err" &
probe=$!
wait_for() {
for _ in $(seq 400); do
grep -qxF "$1" "$work/out" && return 0
kill -0 "$probe" 2>/dev/null || break
sleep 0.25
done
fail "never saw '$1': $(cat "$work/out") $(tail -n 5 "$work/err")"
}
wait_for "probe: hold"
[ "$("$work/press" $$ none)" != untrusted ] || fail "needs Accessibility permission"
# The page's magenta marker: share of the window.
marker() {
local share
share=$("$work/share" "$probe" 255,0,170)
case $share in
denied) fail "needs Screen Recording permission" ;;
none) fail "the probe has no window" ;;
esac
echo "$share"
}
share=$(marker)
awk -v s="$share" 'BEGIN { exit !(s > 0.01) }' || fail "the page is not painted (marker share $share)"
# A killed renderer: the fault shows, its button reloads.
pkill -P "$probe" -f -- "--type=renderer" || fail "no renderer to stop"
wait_for "probe: main failed"
sleep 1
gone=$(marker)
awk -v s="$gone" 'BEGIN { exit !(s < 0.001) }' || fail "the dead page still shows (marker share $gone)"
pressed=
for _ in $(seq 20); do
[ "$("$work/press" "$probe" Reload)" = pressed ] && pressed=1 && break
sleep 0.25
done
[ -n "$pressed" ] || fail "no Reload button in the fault view"
wait_for "probe: recovered"
sleep 1.5
back=$(marker)
awk -v s="$back" 'BEGIN { exit !(s > 0.01) }' || fail "the page did not come back (marker share $back)"
wait "$probe" || fail "the probe did not exit cleanly: $(tail -n 5 "$work/err")"
probe=
out=$(cat "$work/out")
for want in "probe: value=你好a" "probe: focused=text" "probe: zoom=1.25" "probe: stayed" \
"probe: outside=false" "probe: network=refused" "probe: udp=silent" "probe: rtc=undefined" "probe: main failed" "probe: hash loaded" "probe: missing failed" \
"probe: closed 20" "probe: dropped 5 unmade, 10 early" "probe: inert loaded" "probe: scripted ran" "probe: inert checked"; do
grep -qxF "$want" <<<"$out" || fail "expected '$want', got: $out"
done
! grep -q "probe: inert ran" <<<"$out" || fail "an inert page ran a script: $out"
grep -qE '^probe: fg=rgba\([0-9]+, [0-9]+, [0-9]+, ' <<<"$out" || fail "no theme token: $out"
# Freed before the hold: 20 closed, 5 unmade, 10 early.
freed=$(awk "/probe: hold/ { exit } /a page's memory is freed/ { n++ } END { print n + 0 }" "$work/err")
[ "$freed" -ge 35 ] || fail "only $freed of 35 closed pages freed their memory"
! grep -q "panicked" "$work/err" || fail "panics: $(grep -m3 panicked "$work/err")"
echo "ok: webview (marker share $share, $freed pages freed)"