Files
ElyOffice/tests/scripts/mark-outside.sh
T
ZacharyZhang-NY b7fca0146d Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git),
ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
2026-10-08 21:20:08 -04:00

345 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
# MARK-08, MARK-12, QA-22 live: outside changes, autosave, restore.
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
}
command -v npm >/dev/null || { echo "SKIP: no npm"; exit 0; }
work=$(mktemp -d)
app=
trap 'if [ -n "$app" ]; then kill "$app" 2>/dev/null || true; wait "$app" 2>/dev/null || true; fi; rm -rf "$work"' EXIT
fail() {
echo "FAIL: $*" >&2
exit 1
}
"$root/scripts/build-mark-content.sh" >"$work/build" 2>&1 || fail "build: $(tail -n 5 "$work/build")"
cargo build -q -p ely-mark --manifest-path "$root/Cargo.toml"
for tool in ax-values ax-press; do
swiftc -O -o "$work/$tool" "$root/tests/scripts/$tool.swift"
done
mkdir "$work/notes"
doc=$work/notes/t.md
printf 'one\n\ntwo\n' >"$doc"
launch() {
ELY_LOG=info ELY_LOCALE=en ELY_NO_DIALOG=1 HOME="$work" \
"$root/target/debug/ely-mark" "$@" >"$work/out" 2>"$work/err" &
app=$!
waits "ElyMark window open"
osascript -e "tell application \"System Events\" to set frontmost of (first process whose unix id is $app) to true" -e 'delay 2' >/dev/null
}
waits() {
for _ in $(seq 120); do
grep -qF "$1" "$work/err" && return 0
kill -0 "$app" 2>/dev/null || fail "ElyMark quit: $(tail -n 5 "$work/err")"
sleep 0.25
done
fail "never saw '$1': $(grep -E 'mark|office' "$work/err" | tail -n 5)"
}
count() { grep -c "$1" "$work/err" || true; }
# Waits for `$1` to show more than `$2` times.
more() {
for _ in $(seq 120); do
[ "$(count "$1")" -gt "$2" ] && return 0
sleep 0.25
done
fail "no new '$1': $(grep -E 'mark:|recovery' "$work/err" | tail -n 6)"
}
# Types unpaused: a write lands within the autosave second.
now() { osascript -e "tell application \"System Events\" to keystroke \"$1\"" >/dev/null; }
key() { osascript -e "tell application \"System Events\" to $1" -e 'delay 0.8' >/dev/null; }
press() {
for _ in $(seq 20); do
[ "$("$work/ax-press" "$app" "$1")" = pressed ] && { sleep 0.8; return 0; }
sleep 0.25
done
fail "could not press $1"
}
# The window's own titles; system menus list recent files too.
seen() { "$work/ax-values" "$app" | grep -v '^AXMenu' | grep -qF "title=$1"; }
# Waits for an autosave past the `n` seen so far.
autosaved() {
for _ in $(seq 40); do
[ "$(count "autosaves")" -gt "$1" ] && sleep 1 && return 0
sleep 0.25
done
fail "no autosave: $(tr '\n' '/' <"$doc") $(grep -E 'mark:|office:' "$work/err" | tail -n 5)"
}
is() { [ "$(cat "$1")" = "$2" ] || fail "$3: $(cat "$1")"; }
launch "$doc"
waits 'mark: shown as "visual"'
[ "$("$work/ax-press" $$ none)" != untrusted ] || fail "needs Accessibility permission"
# A clean file follows an atomic replace; typing autosaves.
printf 'one\n\ntwo\n\nthree\n' >"$doc.tmp"
mv "$doc.tmp" "$doc"
waits "changed outside; unsaved false"
sleep 1
before=$(count autosaves)
key 'key code 125 using command down'
key 'keystroke "Z"'
autosaved "$before"
is "$doc" $'one\n\ntwo\n\nthreeZ' "reload then autosave"
# Unsaved text meets an outside write: nothing is overwritten.
now X
printf 'outside\n' >"$doc"
waits "changed outside; unsaved true"
sleep 2
is "$doc" "outside" "an outside write was overwritten"
press "Compare…"
seen "Use the Merge" || fail "no compare dialog"
press "Keep Yours"
waits "settles with Mine"
sleep 1
is "$doc" $'one\n\ntwo\n\nthreeZX' "keep yours"
# Autosave runs again once the user chose.
before=$(count autosaves)
key 'keystroke "K"'
autosaved "$before"
is "$doc" $'one\n\ntwo\n\nthreeZXK' "autosave after keep"
# Edits apart merge; a disk change mid-way compares again.
key 'key code 126 using command down'
conflicts=$(count "unsaved true")
now A
printf 'one\n\ntwo\n\nthreeZXK\n\ntail\n' >"$doc"
more "unsaved true" "$conflicts"
press "Compare…"
compares=$(count "compares tab")
printf 'one\n\ntwo\n\nthreeZXK\n\ntail\n\nmore\n' >"$doc"
press "Use the Merge"
waits "during the comparison"
more "compares tab" "$compares"
is "$doc" $'one\n\ntwo\n\nthreeZXK\n\ntail\n\nmore' "a stale merge was written"
press "Use the Merge"
waits "settles with Merged"
before=$(count autosaves)
autosaved "$before"
is "$doc" $'Aone\n\ntwo\n\nthreeZXK\n\ntail\n\nmore' "merge"
# A rename outside: the tab follows, saves the new name.
mv "$doc" "$work/notes/u.md"
waits "was renamed outside"
seen "u.md" || fail "the tab kept its old name"
before=$(count autosaves)
key 'keystroke "R"'
autosaved "$before"
[ ! -e "$doc" ] || fail "the old name came back"
grep -q '^RAone' "$work/notes/u.md" || fail "rename save: $(cat "$work/notes/u.md")"
# An unreadable change says so; a fresh look recovers.
clean=$(count "unsaved false")
printf 'locked\n' >"$work/notes/u.md"
chmod 000 "$work/notes/u.md"
for _ in $(seq 40); do seen "Check Again" && break; sleep 0.25; done
seen "Check Again" || fail "no offer to look again"
chmod 644 "$work/notes/u.md"
press "Check Again"
more "unsaved false" "$clean"
# A delete keeps the text and never recreates the file.
rm "$work/notes/u.md"
waits "was deleted outside"
seen "Save As…" || fail "no Save As offer"
before=$(count autosaves)
key 'keystroke "D"'
sleep 3
[ "$(count autosaves)" = "$before" ] || fail "autosave ran on a deleted file"
[ ! -e "$work/notes/u.md" ] || fail "autosave recreated a deleted file"
kill "$app"
wait "$app" 2>/dev/null || true
app=
! grep -q "panicked" "$work/err" || fail "panics: $(grep -m3 panicked "$work/err")"
# The killed run above left a crash; start clean.
rm -rf "$work/Library/Application Support/ElyOffice/ely-mark/"{session.json,session.lock,recovery}
# A clean quit keeps the tabs, mode, line and column.
printf 'kept\n\nsecond\n\nthird\n' >"$doc"
gone=$work/notes/w.md
printf 'other\n' >"$gone"
launch "$gone" "$doc"
more 'mark: shown as "visual"' 1
press t.md
key 'keystroke "f" using command down'
key 'key code 53'
key 'keystroke "2" using command down'
waits 'mark: shown as "source"'
key 'key code 125 using command down'
key 'key code 126'
key 'key code 123 using command down'
key 'key code 124'
key 'key code 124'
sleep 1
key 'keystroke "q" using command down'
for _ in $(seq 40); do kill -0 "$app" 2>/dev/null || break; sleep 0.25; done
! kill -0 "$app" 2>/dev/null || fail "Cmd+Q did not quit"
app=
launch
waits "restores tab"
waits 'mark: shown as "source"'
sleep 1
# Restored tabs open in any order; pick t.md's page.
press t.md
key 'keystroke "f" using command down'
key 'key code 53'
before=$(count autosaves)
key 'keystroke "Q"'
autosaved "$before"
is "$doc" $'kept\n\nsecond\n\nthQird' "the restored caret"
# Unsaved text, even a deleted file's, returns as copies.
points=$(count "recovery: a point")
press t.md
conflicts=$(count "unsaved true")
now Y
printf 'disk\n' >"$doc"
more "unsaved true" "$conflicts"
press w.md
# Closing the find box hands the keys to the page.
key 'keystroke "f" using command down'
key 'key code 53'
conflicts=$(count "unsaved true")
now V
printf 'gone\n' >"$gone"
more "unsaved true" "$conflicts"
rm "$gone"
more "recovery: a point" "$((points + 1))"
kill -9 "$app"
wait "$app" 2>/dev/null || true
app=
launch
waits "ended Crashed"
waits "did not open"
for _ in $(seq 40); do [ "$(count "toast .*unsaved text from")" -ge 2 ] && break; sleep 0.25; done
[ "$(count "toast .*unsaved text from")" = 2 ] || fail "recovered copies: $(grep "unsaved text from" "$work/err")"
sleep 3
is "$doc" "disk" "recovered text went into the file"
[ ! -e "$gone" ] || fail "a deleted file came back"
# Each copy holds its own text, past the first sweep.
rec="$work/Library/Application Support/ElyOffice/ely-mark/recovery"
session="$work/Library/Application Support/ElyOffice/ely-mark/session.json"
sleep 12
withy=$(grep -rlF Y "$rec" --include='*.bin' | head -n 1 || true)
withv=$(grep -rlF V "$rec" --include='*.bin' | head -n 1 || true)
[ -n "$withy" ] && [ -n "$withv" ] || fail "a copy lost its text: $(ls -R "$rec")"
! grep -qF V "$withy" && ! grep -qF Y "$withv" || fail "a copy took another's text"
# A cancelled Save panel stops the quit; the list follows.
key 'keystroke "q" using command down'
for _ in $(seq 20); do seen "Don't Save" && break; sleep 0.25; done
[ "$("$work/ax-press" "$app" Save AXButton)" = pressed ] || fail "no Save button"
sleep 2
key 'key code 53'
key 'keystroke "n" using command down'
sleep 2
points=$(count "recovery: a point")
key 'keystroke "third"'
more "recovery: a point" "$points"
for _ in $(seq 60); do [ "$(grep -c '"slot"' "$session")" = 3 ] && ! grep -q '"path": "' "$session" && break; sleep 0.5; done
[ "$(grep -c '"slot"' "$session")" = 3 ] && ! grep -q '"path": "' "$session" || fail "the kept list froze: $(tr -d " \n" <"$session")"
# A discard holds even through a crash right after.
key 'keystroke "q" using command down'
for _ in $(seq 20); do seen "Don't Save" && break; sleep 0.25; done
press "Don't Save"
for _ in $(seq 20); do seen "Don't Save" && break; sleep 0.25; done
press Cancel
kill -9 "$app"
wait "$app" 2>/dev/null || true
app=
launch
waits "ended Crashed"
sleep 4
[ "$(count "toast .*unsaved text from")" = 2 ] || fail "after a discard: $(grep "toast .*unsaved text from" "$work/err")"
key 'keystroke "q" using command down'
for _ in $(seq 4); do
for _ in $(seq 20); do seen "Don't Save" && break; sleep 0.25; done
seen "Don't Save" || break
press "Don't Save"
done
for _ in $(seq 40); do kill -0 "$app" 2>/dev/null || break; sleep 0.25; done
app=
# A discard is not a crash: nothing comes back.
launch
sleep 4
! grep -q "unsaved text from" "$work/err" || fail "discarded text came back"
kill "$app"
wait "$app" 2>/dev/null || true
app=
# A tab discarded, then a crash: it stays gone.
launch
key 'keystroke "n" using command down'
sleep 2
points=$(count "recovery: a point")
key 'keystroke "solo"'
more "recovery: a point" "$points"
[ "$("$work/ax-press" "$app" Close AXButton)" = pressed ] || fail "no tab Close"
for _ in $(seq 20); do seen "Don't Save" && break; sleep 0.25; done
press "Don't Save"
kill -9 "$app"
wait "$app" 2>/dev/null || true
app=
launch
waits "ended Crashed"
sleep 4
! grep -q "unsaved text from" "$work/err" || fail "a discarded tab came back"
kill "$app"
wait "$app" 2>/dev/null || true
app=
# A quit's conflict reloaded stops it; the list follows.
printf 'base\n' >"$doc"
launch "$doc"
waits 'mark: shown as "visual"'
conflicts=$(count "unsaved true")
now L
printf 'other\n' >"$doc"
more "unsaved true" "$conflicts"
key 'keystroke "q" using command down'
for _ in $(seq 20); do seen "Don't Save" && break; sleep 0.25; done
[ "$("$work/ax-press" "$app" Save AXButton)" = pressed ] || fail "no Save button"
for _ in $(seq 20); do seen "Reload from Disk" && break; sleep 0.25; done
press "Reload from Disk"
sleep 2
key 'keystroke "n" using command down'
sleep 2
points=$(count "recovery: a point")
key 'keystroke "afterreload"'
more "recovery: a point" "$points"
for _ in $(seq 60); do [ "$(grep -c '"slot"' "$session")" = 2 ] && break; sleep 0.5; done
[ "$(grep -c '"slot"' "$session")" = 2 ] || fail "the list froze after a reload: $(tr -d " \n" <"$session")"
key 'keystroke "q" using command down'
for _ in $(seq 20); do seen "Don't Save" && break; sleep 0.25; done
press "Don't Save"
for _ in $(seq 40); do kill -0 "$app" 2>/dev/null || break; sleep 0.25; done
app=
# A quit's first save keeps its file next time.
rm -f "$session"
launch
key 'keystroke "n" using command down'
sleep 2
key 'keystroke "savedtext"'
key 'keystroke "q" using command down'
for _ in $(seq 20); do seen "Don't Save" && break; sleep 0.25; done
[ "$("$work/ax-press" "$app" Save AXButton)" = pressed ] || fail "no Save button"
sleep 2
key 'keystroke "g" using {command down, shift down}'
key "keystroke \"$work/notes\""
key 'key code 36'
key 'key code 36'
for _ in $(seq 40); do kill -0 "$app" 2>/dev/null || break; sleep 0.25; done
! kill -0 "$app" 2>/dev/null || fail "the quit did not finish after saving"
app=
saved=$(grep -lF savedtext "$work/notes"/*.md || true)
[ -n "$saved" ] || fail "nothing saved: $(ls "$work/notes")"
launch
waits "restores tab"
kill "$app"
wait "$app" 2>/dev/null || true
app=
# A session kept before columns were still opens.
printf '{"tabs":[{"path":"%s","name":"t.md","mode":"source","place":{"line":1,"scroll":0},"slot":""}]}' "$doc" >"$session"
rm -f "$work/Library/Application Support/ElyOffice/ely-mark/session.lock"
launch
waits "restores tab"
waits 'mark: shown as "source"'
kill "$app"
wait "$app" 2>/dev/null || true
app=
echo "ok: mark outside"