ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git), ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
345 lines
12 KiB
Bash
Executable File
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"
|