ElyMark: the file in MarkdownDocument, no web page

The view holds the component library's MarkdownDocument: the file's text
is the document's text, saved as read when unedited. Find, tables,
attachments, the look and export, which ran in the page, go until their
own tasks rebuild them natively. The window scripts that drove the page
go with it; gpui window tests cover the view.

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 02:37:53 -04:00
1 parent 540637cada
commit 1f2920020a
34 files changed
+342 -4144

No files matched your search

-80
View File
@@ -1,80 +0,0 @@
// ElyMark's page: a failed editor says so (M-13).
import { createRequire } from 'node:module'
import { existsSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
const root = join(dirname(fileURLToPath(import.meta.url)), '../..')
const page = join(root, 'engines/content/ely-mark/index.html')
if (!existsSync(page)) {
console.log('SKIP: no page; run scripts/build-mark-content.sh')
process.exit(0)
}
let chromium
try {
;({ chromium } = createRequire(join(root, 'site/package.json'))('@playwright/test'))
} catch {
console.log('SKIP: no Playwright; run pnpm install in site')
process.exit(0)
}
const fail = (why) => {
console.error(`FAIL: ${why}`)
process.exit(1)
}
const browser = await chromium.launch()
const tab = await browser.newPage()
await tab.addInitScript(() => {
window.asked = []
window.elyQuery = ({ request, onSuccess }) => {
window.asked.push(JSON.parse(request))
onSuccess('')
}
// Setting a role throws; `spare` keeps CodeMirror's.
const set = Element.prototype.setAttribute
window.breaks = (spare) => {
Element.prototype.setAttribute = function (name, value) {
const kept = spare && this.classList.contains('cm-content')
if (name === 'role' && !kept) throw new Error('broken')
return set.call(this, name, value)
}
}
window.mends = () => {
Element.prototype.setAttribute = set
}
})
await tab.goto(pathToFileURL(page).href)
// What the app heard since the last look.
let seen = 0
const heard = async () => {
await tab.waitForTimeout(800)
const all = await tab.evaluate(() => window.asked.filter((a) => a.kind === 'shown' || a.kind === 'failed'))
const fresh = all.slice(seen)
seen = all.length
return JSON.stringify(fresh)
}
const holders = () => tab.evaluate(() => document.querySelectorAll('#document > div').length)
const shown = () => tab.evaluate(() => document.querySelector('#document .cm-content')?.textContent ?? '')
const open = (mode) => tab.evaluate((mode) => window.ely.open('# Kept\n', mode, {}, 1, '', null), mode)
await tab.evaluate(() => window.breaks(true))
await open('visual')
const fell = JSON.stringify([{ kind: 'shown', mode: 'source', fallback: true, broke: 'Error' }])
if ((await heard()) !== fell) fail('a broken visual editor did not fall back to source')
if ((await shown()) !== '# Kept') fail(`the fallback shows ${JSON.stringify(await shown())}`)
await tab.evaluate(() => window.breaks(false))
const failed = JSON.stringify([{ kind: 'failed', generation: 1, what: 'Error' }])
for (let n = 0; n < 3; n++) {
await open('visual')
if ((await heard()) !== failed) fail(`two broken editors, try ${n}: the app was not told`)
}
if ((await holders()) !== 0) fail(`failed builds left ${await holders()} holders`)
await tab.evaluate(() => window.mends())
await tab.evaluate(() => window.ely.mode('source'))
const back = JSON.stringify([{ kind: 'shown', mode: 'source', fallback: false, broke: null }])
if ((await heard()) !== back) fail('a mode after the failure did not rebuild')
if ((await shown()) !== '# Kept' || (await holders()) !== 1) fail('the rebuilt editor lost the text')
await browser.close()
console.log('mark-broken: fallback, told failures, no leftover holders, a mode rebuilds')
-123
View File
@@ -1,123 +0,0 @@
#!/usr/bin/env bash
# ElyMark's built page renders each package offline, live.
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)
probe=
trap 'if [ -n "$probe" ]; then kill "$probe" 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")"
content=$root/engines/content/ely-mark
for name in @milkdown/kit @codemirror/view katex mermaid; do
grep -q "^$name " "$content/THIRD-PARTY-NOTICES.txt" || fail "no notice for $name"
done
cargo build -q -p ely-mark --example markprobe --manifest-path "$root/Cargo.toml"
swiftc -O -o "$work/share" "$root/tests/scripts/window-share.swift"
ELY_LOG=info ELY_LOCALE=en ELY_SMOKE=hold HOME="$work" XDG_DATA_HOME="$work/data" \
perl -e 'alarm 120; exec @ARGV' "$root/target/debug/examples/markprobe" >"$work/out" 2>"$work/err" &
probe=$!
for _ in $(seq 240); do
grep -q '"step":"visual"' "$work/out" && break
kill -0 "$probe" 2>/dev/null || break
sleep 0.25
done
grep -q '"step":"visual"' "$work/out" || fail "no visual report: $(tail -n 5 "$work/err")"
# Less than all background: the page is drawn.
bg=$(grep '"step":"visual"' "$work/out" | grep -oE '"bg":"rgba\([0-9]+, [0-9]+, [0-9]+' | grep -oE '[0-9]+, [0-9]+, [0-9]+$' | tr -d ' ')
share=$("$work/share" "$probe" "$bg")
case $share in
denied) fail "needs Screen Recording permission" ;;
none) fail "the probe has no window" ;;
esac
awk -v s="$share" 'BEGIN { exit !(s < 0.97) }' || fail "the page is not on screen (background share $share)"
wait "$probe" || fail "the probe failed: $(tail -n 5 "$work/err")"
probe=
grep -qxF "mark: done" "$work/out" || fail "unfinished: $(cat "$work/out")"
# The earlier of two quick shows never reports.
shows=$(grep '^mark: shown' "$work/out" | tr -d '"' | cut -d' ' -f3 | tr '\n' ' ')
[ "$shows" = "visual preview source source visual visual preview source source visual preview source visual source source visual preview source source source visual visual visual " ] || fail "shows reported: $shows"
python3 - "$work/out" <<'PY'
import json, sys
seen = {}
for line in open(sys.argv[1]):
if line.startswith("mark: {"):
read = json.loads(line[6:])
seen[read["step"]] = read
def want(step, key, value):
if seen[step][key] != value:
sys.exit(f"FAIL: {step} {key} is {seen[step][key]!r}, not {value!r}")
languages = ["rust", "LaTeX", "LaTeX", "mermaid", "mermaid"]
for step in ("visual", "themed", "preview"):
for key, value in [("title", "Title"), ("heading", True), ("inline", True), ("display", True), ("tasks", 2),
("cells", 2), ("notes", 1), ("font", True), ("editors", 1)]:
want(step, key, value)
# Every task box: text-sized, centred on its line.
for width, height, off, drawn in seen[step]["boxes"]:
if not drawn or abs(width - 1) > 0.01 or abs(height - 1) > 0.01 or abs(off) > 1:
sys.exit(f"FAIL: {step} task boxes are {seen[step]['boxes']}")
blocks = seen[step]["blocks"]
if [block["language"] for block in blocks] != languages:
sys.exit(f"FAIL: {step} blocks are {blocks}")
# Preview shows a block's preview alone, when it has one.
for at, block in enumerate(blocks):
if block["editors"] != 1 or block["code"] != (step != "preview" or at == 0):
sys.exit(f"FAIL: {step} block {at} code is not shown: {block}")
width, height = blocks[3]["diagram"]
if not blocks[1]["math"] or not blocks[3]["drawn"] or width < 100 or height < 80:
sys.exit(f"FAIL: {step} math or diagram is not shown: {blocks}")
if "(9)" not in blocks[2]["failure"] or "line 3" not in blocks[4]["failure"]:
sys.exit(f"FAIL: {step} errors lack source or place: {blocks}")
editable = "true" if step != "preview" else "false"
if any(block["editable"] != editable for block in blocks):
sys.exit(f"FAIL: {step} code blocks are not {editable}: {blocks}")
fills = [seen[step]["blocks"][3]["fill"] for step in ("visual", "themed")]
if not fills[0] or fills[0] == fills[1]:
sys.exit(f"FAIL: the diagram kept its colors: {fills}")
want("visual", "editable", "true")
want("preview", "editable", "false")
want("source", "first", "# Title")
want("source", "source", True)
want("source", "blocks", [])
want("race", "mode", "source")
want("race", "first", "# NEW")
want("race", "editors", 1)
want("asked", "mode", "preview")
want("stale", "mode", "source")
want("owner", "mode", "source")
# Two broken patterns per mode error; empty ones open nothing.
if seen["find"]["panels"] != {"source": 0, "visual": 0} or seen["find"]["errors"] != 4:
sys.exit(f"FAIL: find: {seen['find']}")
# A zero-width match after an emoji: seven hits, per mode.
if seen["find"]["totals"].count(7) != 2:
sys.exit(f"FAIL: zero-width find: {seen['find']}")
look = seen["look"]
# 36rem at 120 percent of 16px: 691.2px.
if look["dim"] != look["blocks"] - 1 or look["bright"] != "1" or not look["centered"] or look["width"] != "691.2px" or not look["serif"]:
sys.exit(f"FAIL: the writing view: {look}")
if "OLD" in seen["race"]["text"]:
sys.exit("FAIL: the earlier show stayed")
# A diagram begun before a change never lands after.
language = seen["language"]["blocks"][0]
if language["language"].lower() != "rust" or language["drawn"] or language["diagram"] != [0, 0]:
sys.exit(f"FAIL: an old diagram stayed in a Rust block: {language}")
late = seen["midtheme"]["blocks"][0]
if not late["drawn"] or {late["fill"], late["first"]} != {seen["visual"]["blocks"][3]["fill"]}:
sys.exit(f"FAIL: a diagram drawn mid-change showed the old theme: {late}")
# Draws for a gone editor never run.
if seen["release"]["long"] > 1:
sys.exit(f"FAIL: {seen['release']['long']} long stalls after leaving the diagrams")
PY
! grep -q "refused" "$work/err" || fail "the page reached outside: $(grep -m3 refused "$work/err")"
! grep -q "panicked" "$work/err" || fail "panics: $(grep -m3 panicked "$work/err")"
echo "ok: mark content (background share $share)"
-76
View File
@@ -1,76 +0,0 @@
// ElyMark's page: which clicks on links reach the app (M-05).
import { createRequire } from 'node:module'
import { existsSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
const root = join(dirname(fileURLToPath(import.meta.url)), '../..')
const page = join(root, 'engines/content/ely-mark/index.html')
if (!existsSync(page)) {
console.log('SKIP: no page; run scripts/build-mark-content.sh')
process.exit(0)
}
let chromium
try {
;({ chromium } = createRequire(join(root, 'site/package.json'))('@playwright/test'))
} catch {
console.log('SKIP: no Playwright; run pnpm install in site')
process.exit(0)
}
const fail = (why) => {
console.error(`FAIL: ${why}`)
process.exit(1)
}
const browser = await chromium.launch()
const tab = await browser.newPage()
await tab.addInitScript(() => {
window.asked = []
window.elyQuery = ({ request, onSuccess }) => {
window.asked.push(JSON.parse(request))
onSuccess('')
}
})
const left = []
await tab.route('https://example.com/**', (route) => {
left.push(route.request().url())
route.abort()
})
await tab.goto(pathToFileURL(page).href)
const doc = 'Go [web](https://example.com/) or [ftp](ftp://x.example/) and [one](https://one.example/)[two](https://two.example/) end.\n\n' +
'[local](file:///outside/file.md) [data](data:text/plain,hello) [empty]()\n'
const links = async () => (await tab.evaluate(() => window.asked)).filter((a) => a.kind === 'link').map((a) => a.href)
const opener = (await tab.evaluate(() => navigator.platform)).startsWith('Mac') ? 'Meta' : 'Control'
const anchor = (text) => tab.locator('a', { hasText: new RegExp(`^${text}$`) })
// Clicks `dx` from the anchor's left edge, mid-height.
const at = async (text, dx, modifiers) => {
const box = await anchor(text).boundingBox()
await tab.mouse.move(box.x + dx, box.y + box.height / 2)
for (const key of modifiers) await tab.keyboard.down(key)
await tab.mouse.down()
await tab.mouse.up()
for (const key of modifiers) await tab.keyboard.up(key)
}
await tab.evaluate((doc) => window.ely.open(doc, 'visual', {}, 1, '', null), doc)
await anchor('web').click()
if ((await links()).length) fail('a plain click in the editor opened a link')
await at('web', 1, [opener])
await at('web', (await anchor('web').boundingBox()).width + 3, [opener])
await at('two', 1, [opener])
await anchor('ftp').click({ modifiers: [opener] })
const asked = await links()
const want = 'https://example.com/ https://two.example/ ftp://x.example/'
if (asked.join(' ') !== want) fail(`the editor asked ${JSON.stringify(asked)}, not ${want}`)
await tab.evaluate((doc) => window.ely.open(doc, 'preview', {}, 2, '', null), doc)
const before = (await links()).length
const here = tab.url()
for (const text of ['web', 'local', 'data', 'empty']) await anchor(text).click()
await tab.waitForTimeout(500)
const opened = (await links()).slice(before)
const sent = 'https://example.com/ file:///outside/file.md data:text/plain,hello '
if (opened.join(' ') !== sent) fail(`preview asked ${JSON.stringify(opened)}, not ${JSON.stringify(sent)}`)
if (left.length || tab.url() !== here) fail(`preview navigated: ${JSON.stringify(left)} ${tab.url()}`)
await browser.close()
console.log('mark-links: an editor click edits, the opener key asks the app, preview asks the app')
-179
View File
@@ -1,179 +0,0 @@
#!/usr/bin/env bash
# MARK-01 to 04 live: modes, images, new and rename.
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"
swiftc -O -o "$work/share" "$root/tests/scripts/window-share.swift"
# A magenta picture beside the document.
python3 - "$work/pic.png" <<'PY'
import struct, sys, zlib
w, h = 240, 120
rows = b"".join(b"\0" + b"\xff\x00\xaa" * w for _ in range(h))
def chunk(kind, data):
return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data))
png = b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0))
png += chunk(b"IDAT", zlib.compress(rows)) + chunk(b"IEND", b"")
open(sys.argv[1], "wb").write(png)
PY
text=$'---\ntitle: Modes\n---\n# Modes\n\nFirst line.\n\n![pic](pic.png)\n\n- [ ] task\n\nLast line.\n'
printf '%s' "$text" >"$work/doc.md"
cp "$work/doc.md" "$work/orig.md"
ELY_LOG=info ELY_LOCALE=en ELY_NO_DIALOG=1 HOME="$work" XDG_DATA_HOME="$work/data" \
"$root/target/debug/ely-mark" "$work/doc.md" >"$work/out" 2>"$work/err" &
app=$!
shows() { grep -c "mark: shown as" "$work/err" || true; }
# Waits for show number `n`, as `mode`.
wait_shown() {
for _ in $(seq 120); do
[ "$(shows)" -ge "$1" ] && break
kill -0 "$app" 2>/dev/null || fail "ElyMark quit: $(tail -n 5 "$work/err")"
sleep 0.25
done
[ "$(grep "mark: shown as" "$work/err" | sed -n "$1p" | grep -c "\"$2\"")" = 1 ] ||
fail "show $1 is not $2: $(grep 'mark: shown' "$work/err")"
}
key() { osascript -e "tell application \"System Events\" to $1" -e 'delay 0.8' >/dev/null; }
saved() {
local before
before=$(grep -c "office: saved" "$work/err" || true)
key 'keystroke "s" using command down'
for _ in $(seq 40); do
[ "$(grep -c "office: saved" "$work/err" || true)" -gt "$before" ] && return 0
sleep 0.25
done
fail "no save"
}
same() { cmp -s "$work/orig.md" "$1" || fail "$2: $(cat "$1")"; }
wait_shown 1 visual
osascript -e "tell application \"System Events\" to set frontmost of (first process whose unix id is $app) to true" -e 'delay 1' >/dev/null
share=$("$work/share" "$app" 255,0,170)
case $share in denied) fail "needs Screen Recording permission" ;; none) fail "no window" ;; esac
awk -v s="$share" 'BEGIN { exit !(s > 0.003) }' || fail "the picture beside the file did not show ($share)"
# Switching alone changes nothing.
key 'keystroke "2" using command down'
wait_shown 2 source
key 'keystroke "3" using command down'
wait_shown 3 preview
key 'keystroke "1" using command down'
wait_shown 4 visual
saved
same "$work/doc.md" "switching modes changed the file"
# Preview takes no typing.
key 'keystroke "3" using command down'
wait_shown 5 preview
key 'keystroke "z"'
key 'keystroke "1" using command down'
wait_shown 6 visual
saved
same "$work/doc.md" "preview took typing"
# Visual edits undo after a trip through preview.
key 'keystroke "y"'
key 'keystroke "3" using command down'
wait_shown 7 preview
key 'keystroke "1" using command down'
wait_shown 8 visual
key 'keystroke "z" using command down'
wait_shown 9 visual
saved
same "$work/doc.md" "undo did not survive the trip"
# A source edit shows in visual and saves.
key 'keystroke "2" using command down'
wait_shown 10 source
key 'keystroke "x"'
key 'keystroke "1" using command down'
wait_shown 11 visual
saved
[ "$(cat "$work/doc.md"; echo .)" = "x$text." ] || fail "source edit: $(cat "$work/doc.md")"
# One timeline: undo walks back through both modes, redo returns.
key 'keystroke "v"'
key 'keystroke "2" using command down'
wait_shown 12 source
key 'keystroke "s"'
key 'keystroke "1" using command down'
wait_shown 13 visual
key 'keystroke "z" using command down'
wait_shown 14 visual
key 'keystroke "z" using command down'
wait_shown 15 visual
saved
[ "$(cat "$work/doc.md"; echo .)" = "x$text." ] || fail "undo across modes: $(cat "$work/doc.md")"
key 'keystroke "z" using {command down, shift down}'
wait_shown 16 visual
saved
[ "$(cat "$work/doc.md"; echo .)" = "vx$text." ] || fail "redo across modes: $(cat "$work/doc.md")"
key 'keystroke "z" using command down'
wait_shown 17 visual
saved
# Rename refuses a taken name, moves to a free one.
printf 'other\n' >"$work/taken.md"
key 'key code 120'
key 'keystroke "a" using command down'
key 'keystroke "taken.md"'
key 'key code 36'
for _ in $(seq 20); do grep -q "did not rename" "$work/err" && break; sleep 0.25; done
grep -q "did not rename" "$work/err" || fail "a rename onto a file went through"
[ "$(cat "$work/taken.md")" = other ] || fail "the taken file changed"
key 'key code 120'
key 'keystroke "a" using command down'
key 'keystroke "moved.md"'
key 'key code 36'
for _ in $(seq 20); do grep -q "renamed .*moved.md" "$work/err" && break; sleep 0.25; done
[ -f "$work/moved.md" ] && [ ! -e "$work/doc.md" ] || fail "rename: $(ls "$work")"
recent=$(find "$work/Library" -name recent.json)
grep -q 'moved.md' "$recent" && ! grep -q 'doc.md' "$recent" || fail "recent after rename: $(cat "$recent")"
key 'keystroke "w"'
saved
[ "$(cat "$work/moved.md"; echo .)" = "wx$text." ] || fail "save after rename: $(cat "$work/moved.md")"
# Undone steps still redo after a mode trip.
key 'keystroke "2" using command down'
wait_shown 18 source
key 'keystroke "q"'
key 'keystroke "z" using command down'
key 'keystroke "3" using command down'
wait_shown 19 preview
key 'keystroke "2" using command down'
wait_shown 20 source
key 'keystroke "z" using {command down, shift down}'
wait_shown 21 source
saved
[ "$(cat "$work/moved.md"; echo .)" = "qwx$text." ] || fail "source redo after a trip: $(cat "$work/moved.md")"
key 'keystroke "1" using command down'
wait_shown 22 visual
key 'keystroke "p"'
key 'keystroke "z" using command down'
key 'keystroke "3" using command down'
wait_shown 23 preview
key 'keystroke "1" using command down'
wait_shown 24 visual
key 'keystroke "z" using {command down, shift down}'
wait_shown 25 visual
saved
[ "$(cat "$work/moved.md"; echo .)" = "pqwx$text." ] || fail "visual redo after a trip: $(cat "$work/moved.md")"
# New opens an empty visual document.
key 'keystroke "n" using command down'
wait_shown 26 visual
fresh=$(find "$work/Library" -name untitled.md)
[ -n "$fresh" ] && [ ! -s "$fresh" ] || fail "the new document is not empty: $fresh"
key 'keystroke "q" using command down'
key 'key code 36'
for _ in $(seq 40); do kill -0 "$app" 2>/dev/null || break; sleep 0.25; done
app=
! grep -q "panicked" "$work/err" || fail "panics: $(grep -m3 panicked "$work/err")"
echo "ok: mark modes (picture share $share)"
-229
View File
@@ -1,229 +0,0 @@
#!/usr/bin/env bash
# MARK-09, 11, 13, 14 live: look, output, Git, large files.
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; }
command -v pdftotext >/dev/null || { echo "SKIP: no pdftotext"; exit 0; }
[ -x "$root/engines/git/bin/git" ] || export ELY_GIT=${ELY_GIT:-$(command -v git)}
work=$(mktemp -d)
app=
trap 'if [ -n "$app" ]; then kill "$app" 2>/dev/null || true; wait "$app" 2>/dev/null || true; fi; pkill -f "$work" 2>/dev/null || true; 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 -p ely-tree -p ely-pdf --manifest-path "$root/Cargo.toml"
for tool in ax-values ax-press; do
swiftc -O -o "$work/$tool" "$root/tests/scripts/$tool.swift"
done
repo=$work/repo
mkdir -p "$repo/assets"
python3 -c 'import struct,zlib,sys
c=lambda t,d: struct.pack(">I",len(d))+t+d+struct.pack(">I",zlib.crc32(t+d))
open(sys.argv[1],"wb").write(b"\x89PNG\r\n\x1a\n"+c(b"IHDR",struct.pack(">IIBBBBB",1,1,8,2,0,0,0))+c(b"IDAT",zlib.compress(b"\0\xff\0\0"))+c(b"IEND",b""))' "$repo/assets/pic.png"
cp "$repo/assets/pic.png" "$repo/assets/high.png"
doc=$repo/t.md
printf '%s\n' '# Title' '' 'Some $x^2$ math.' '' '![pic](assets/pic.png)' '' "<img src='assets/pic.png'>" '' \
"<img alt='a > b' src='assets/p&#105;c.png'>" '' '```mermaid' 'graph TD; A-->B' '```' '' \
'```mermaid' 'graph TD; SECRET_WORDS -->' '```' '' '<div>RAW TEXT</div><div>TWO</div>' '' \
"<img src='assets/pic.png' srcset='assets/high.png 1x'>" '' "<img srcset=' assets/high.png 1x, assets/pic.png 2x'>" '' \
"<picture><source srcset='assets/high.png'><img src='assets/pic.png'></picture>" '' \
'$\privateDocumentToken$' '' '![gone](missing.png)' '' 'END MARK' >"$doc"
git -C "$repo" init -q
git -C "$repo" -c user.name=t -c user.email=t@e.x add -A
git -C "$repo" -c user.name=t -c user.email=t@e.x commit -qm one
original=$(cat "$doc")
launch() {
ELY_LOG=info ELY_LOCALE=en ELY_NO_DIALOG=1 HOME="$work" ELY_PRINT_TO="$work/printed.pdf" \
"$root/target/debug/ely-mark" "$@" >"$work/log" 2>&1 &
app=$!
waits "mark: shown as"
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 160); do
grep -qF "$1" "$work/log" && return 0
kill -0 "$app" 2>/dev/null || fail "ElyMark quit: $(tail -n 5 "$work/log")"
sleep 0.25
done
fail "never saw '$1': $(grep -E 'mark:|office:' "$work/log" | tail -n 5)"
}
count() { grep -c "$1" "$work/log" || true; }
front() { osascript -e "tell application \"System Events\" to set frontmost of (first process whose unix id is $app) to true" -e 'delay 1' >/dev/null; }
key() { osascript -e "tell application \"System Events\" to $1" -e 'delay 0.8' >/dev/null; }
# The window's own titles; system menus list recent files too.
seen() { "$work/ax-values" "$app" | grep -v '^AXMenu' | grep -qF "title=$1"; }
press() {
for _ in $(seq 40); do
[ "$("$work/ax-press" "$app" "$@")" = pressed ] && { sleep 0.8; return 0; }
sleep 0.25
done
fail "could not press $*"
}
quit() {
kill "$app"
wait "$app" 2>/dev/null || true
app=
}
# Asks a `kind` export beside the document, maybe named `$2`.
ask_export() {
front
key 'keystroke "e" using command down'
press "$1"
press "Export…" AXButton
# The panel opens in the document's folder, the stem chosen.
sleep 3
[ -z "${2:-}" ] || key "keystroke \"$2\""
# The panel's own button: Return alone did not always save.
press Save AXButton
}
# Exports `kind`; waits for its log line.
export_as() {
local before failed
before=$(count "mark: exported")
failed=$(count "export failed")
ask_export "$@"
for _ in $(seq 160); do
[ "$(count "mark: exported")" -gt "$before" ] && return 0
[ "$(count "export failed")" -gt "$failed" ] && fail "export: $(grep "export failed" "$work/log" | tail -n 1)"
sleep 0.25
done
fail "no $1 export: $(grep -E "mark:|office:|dialog|panel" "$work/log" | tail -n 4)"
}
launch "$doc"
[ "$("$work/ax-press" $$ none)" != untrusted ] || fail "needs Accessibility permission"
# Git sees the file; its item opens ElyTree there.
for _ in $(seq 40); do seen "Git: unchanged" && break; sleep 0.25; done
seen "Git: unchanged" || fail "no Git state: $(grep 'git says' "$work/log" | tail -n 1)"
key 'keystroke "f" using command down'
key 'key code 53'
key 'key code 125 using command down'
key 'keystroke "Z"'
for _ in $(seq 60); do seen "Git: changed" && break; sleep 0.25; done
seen "Git: changed" || fail "Git missed the change: $(grep -E "git says|autosave|saved" "$work/log" | tail -n 4)"
printf '%s\n' "$original" >"$doc"
press "Git: changed"
for _ in $(seq 40); do pgrep -f "target/debug/ely-tree .*${repo#/private}" >/dev/null && break; sleep 0.25; done
pgrep -f "target/debug/ely-tree .*${repo#/private}" >/dev/null || fail "ElyTree did not open the repository"
# ElyTree takes the file itself and shows its history (§16).
waits "tree: file history of t.md"
pkill -f "target/debug/ely-tree .*${repo#/private}" || true
sleep 2
front
# Focus mode folds the chrome; the look outlives the app.
press "Focus Mode"
! seen "t.md" || fail "focus mode kept the tabs"
press "Serif Font"
quit
grep -q '"focus": true' "$work/Library/Application Support/ElyOffice/ely-mark/look.json" || fail "the look was not kept"
[ "$(cat "$doc")" = "$original" ] || fail "the look changed the file: $(cat "$doc")"
printf '# Other\n' >"$repo/b.md"
launch "$repo/b.md" "$doc"
! seen "t.md" || fail "focus mode did not come back"
press "Focus Mode"
seen "t.md" || fail "focus mode stayed"
# Tabs open in any order; t.md's page takes the keys.
press t.md
key 'keystroke "f" using command down'
key 'key code 53'
# Each export holds the whole document.
export_as HTML
html=$repo/t.html
grep -q 'class="katex' "$html" && grep -q '<svg' "$html" && grep -q 'END MARK' "$html" ||
fail "the HTML lacks a part"
# Both image forms inline; what was left out is said.
[ "$(grep -o 'data:image/png;base64' "$html" | wc -l | tr -d ' ')" = 6 ] || fail "an image stayed outside"
! grep -qE 'srcset|assets/' "$html" || fail "a responsive source stayed outside"
grep -q "without 1 images and 1 diagrams; formulas failed at lines \[27\]" "$work/log" ||
fail "the lost parts went unsaid: $(grep 'mark: exported' "$work/log" | tail -n 1)"
! grep -qE 'SECRET_WORDS|privateDocumentToken|missing\.png' "$work/log" || fail "the log holds document text: $(grep -oE '.{0,80}(SECRET_WORDS|privateDocumentToken)' "$work/log" | head -n 1)"
# A name with `#` still links its folder.
export_as "HTML and Folder" "report#1"
linked=$repo/report#1.html
grep -q 'href="report%231_files/export.css"' "$linked" && grep -q 'src="report%231_files/1-pic.png"' "$linked" &&
[ -s "$repo/report#1_files/export.css" ] || fail "the folder export: $(grep -o 'href="[^"]*"' "$linked")"
! grep -qE 'srcset|assets/' "$linked" && [ -s "$repo/report#1_files/2-high.png" ] &&
[ "$(grep -o 'src="report%231_files/[^"]*png"' "$linked" | wc -l | tr -d ' ')" = 6 ] ||
fail "the folder lacks a responsive image: $(ls "$repo/report#1_files")"
# A PDF never lands on the open file via links.
ln -s b.md "$repo/other.pdf"
ask_export PDF other
for _ in $(seq 20); do seen Replace && break; sleep 0.25; done
press Replace AXButton
for _ in $(seq 80); do grep -q "is open in ElyMark" "$work/log" && break; sleep 0.25; done
grep -q "is open in ElyMark" "$work/log" || fail "the export wrote over another open tab"
[ "$(cat "$repo/b.md")" = "# Other" ] || fail "another open file was overwritten"
rm "$repo/other.pdf"
ln -s t.md "$repo/alias.pdf"
ask_export PDF alias
for _ in $(seq 20); do seen Replace && break; sleep 0.25; done
press Replace AXButton
for _ in $(seq 80); do grep -q "export failed" "$work/log" && break; sleep 0.25; done
grep -q "export failed" "$work/log" || fail "the export wrote through the link"
grep -q '^# Title' "$doc" || fail "the open file was overwritten: $(head -c 20 "$doc")"
rm "$repo/alias.pdf"
export_as PDF
[ "$(head -c 5 "$repo/t.pdf")" = "%PDF-" ] || fail "no PDF"
# The toast's Open shows the PDF in ElyPDF (§16).
press Open AXButton
for _ in $(seq 40); do pgrep -f "target/debug/ely-pdf .*t.pdf" >/dev/null && break; sleep 0.25; done
pgrep -f "target/debug/ely-pdf .*t.pdf" >/dev/null || fail "Open did not show the PDF in ElyPDF"
pkill -f "target/debug/ely-pdf .*t.pdf" || true
front
pdftotext "$repo/t.pdf" - | grep -q "END MARK" || fail "the PDF lacks text"
[ "$(grep -c 'without 1 images' "$work/log")" -ge 3 ] || fail "the PDF hid a missing image"
export_as "Plain Text"
grep -q 'x^2' "$repo/t.txt" && grep -qx 'RAW TEXT' "$repo/t.txt" && grep -qx 'TWO' "$repo/t.txt" && grep -q 'END MARK' "$repo/t.txt" || fail "the text: $(cat "$repo/t.txt")"
# Print saves each job to a file here.
done_ok() { count "office: print done, printed true"; }
printed=$(done_ok)
key 'keystroke "p" using command down'
for _ in $(seq 60); do [ "$(done_ok)" -gt "$printed" ] && break; sleep 0.25; done
[ "$(done_ok)" -gt "$printed" ] || fail "print made no job: $(grep -E "mark: job|print" "$work/log" | tail -n 4)"
pdftotext "$work/printed.pdf" - | grep -q "END MARK" || fail "the printed job lacks the text"
# Two prints at once: each job renders and prints.
rm "$work/printed.pdf"
printed=$(done_ok)
jobs=$(count "mark: job .* ok true")
osascript -e 'tell application "System Events" to keystroke "p" using command down' -e 'tell application "System Events" to keystroke "p" using command down' >/dev/null
for _ in $(seq 80); do [ "$(done_ok)" -ge $((printed + 2)) ] && break; sleep 0.25; done
[ "$(done_ok)" -ge $((printed + 2)) ] || fail "a second print cancelled the first: $(grep -E "mark: job|print" "$work/log" | tail -n 6)"
[ "$(grep -o "mark: job [0-9]* printed .*, ok true" "$work/log" | tail -n 2 | cut -d' ' -f3 | sort -u | wc -l)" -eq 2 ] || fail "the two prints were not two jobs"
[ "$(count "mark: job .* ok true")" -ge $((jobs + 2)) ] || fail "a job did not render"
pdftotext "$work/printed.pdf" - | grep -q "END MARK" || fail "the last job lacks the text"
# Rich copy carries HTML; it overwrites the clipboard.
if [ "${ELY_CLIPBOARD_TEST:-}" = 1 ]; then
key 'keystroke "f" using command down'
key 'key code 53'
key 'keystroke "a" using command down'
key 'keystroke "c" using command down'
osascript -e 'clipboard info' | grep -q 'HTML' || fail "the copy carried no HTML: $(osascript -e 'clipboard info')"
fi
quit
# A large file: source alone, every byte kept and exported.
big=$work/big.md
python3 -c 'import sys
open(sys.argv[1],"w").write("".join(f"Line {n} of a large file.\n\n" for n in range(120000))+"LAST LINE\n")' "$big"
size=$(wc -c <"$big")
launch "$big"
waits 'mark: shown as "source"'
key 'keystroke "1" using command down'
waits "a large file stays in source"
key 'key code 125 using command down'
# A digit: an input method never holds it.
key 'keystroke "9"'
for _ in $(seq 40); do grep -q 'LAST LINE' "$big" && [ "$(wc -c <"$big")" -gt "$size" ] && break; sleep 0.25; done
[ "$(wc -c <"$big")" -gt "$size" ] && grep -q 'LAST LINE' "$big" && grep -q '^Line 0 of' "$big" || fail "the large file lost bytes"
export_as "Plain Text"
grep -q 'LAST LINE' "$work/big.txt" || fail "a large export was cut"
quit
echo "ok: mark output"
-457
View File
@@ -1,457 +0,0 @@
#!/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"
# Unsent keys survive a touch that keeps the text.
before=$(count autosaves)
changed=$(count "changed outside")
now QQ
touch "$doc"
more "changed outside" "$changed"
autosaved "$before"
is "$doc" $'one\n\ntwo\n\nthreeZXKQQ' "keys typed before a touch were lost"
before=$(count autosaves)
key 'key code 51'
key 'key code 51'
autosaved "$before"
is "$doc" $'one\n\ntwo\n\nthreeZXK' "keys taken back"
# Same text, new bytes: saves keep the new bytes.
lf=$(od -An -tx1 "$doc" | tr -d ' \n')
python3 -c 'import sys; d = open(sys.argv[1], "rb").read(); open(sys.argv[2], "wb").write(b"\xef\xbb\xbf" + d.replace(b"\n", b"\r\n"))' "$doc" "$work/crlf"
# BOM, only CRLF line ends, and `$1` in it.
kept() { python3 -c 'import sys; d = open(sys.argv[1], "rb").read(); sys.exit(not (d.startswith(b"\xef\xbb\xbf") and d.count(b"\n") == d.count(b"\r\n") and sys.argv[2].encode() in d))' "$doc" "$1"; }
changed=$(count "changed outside")
cp "$work/crlf" "$doc"
more "changed outside" "$changed"
before=$(count autosaves)
now W
autosaved "$before"
kept W || fail "a save lost the new bytes: $(od -c "$doc" | head -3)"
before=$(count autosaves)
key 'key code 51'
autosaved "$before"
cmp -s "$doc" "$work/crlf" || fail "the save is not the new bytes: $(od -c "$doc" | head -3)"
changed=$(count "changed outside")
printf '%s' "$lf" | xxd -r -p >"$doc"
more "changed outside" "$changed"
# A conflict the disk takes back resumes autosave.
cp "$doc" "$work/saved"
conflicts=$(count "unsaved true")
now X
printf 'external\n' >"$doc"
more "unsaved true" "$conflicts"
cp "$work/saved" "$doc"
waits "local edits stay"
before=$(count autosaves)
now Y
autosaved "$before"
is "$doc" $'one\n\ntwo\n\nthreeZXKXY' "autosave after the disk came back"
before=$(count autosaves)
key 'key code 51'
key 'key code 51'
autosaved "$before"
is "$doc" $'one\n\ntwo\n\nthreeZXK' "keys taken back again"
# Unsaved keys meet the saved text in new bytes.
cp "$doc" "$work/saved"
python3 -c 'import sys; d = open(sys.argv[1], "rb").read(); open(sys.argv[2], "wb").write(b"\xef\xbb\xbf" + d.replace(b"\n", b"\r\n"))' "$doc" "$work/crlf"
stays=$(count "local edits stay")
before=$(count autosaves)
now V
cp "$work/crlf" "$doc"
more "local edits stay" "$stays"
autosaved "$before"
kept V || fail "edits kept lost the new bytes: $(od -c "$doc" | head -3)"
before=$(count autosaves)
key 'key code 51'
autosaved "$before"
cmp -s "$doc" "$work/crlf" || fail "not the new bytes: $(od -c "$doc" | head -3)"
changed=$(count "changed outside")
cp "$work/saved" "$doc"
more "changed outside" "$changed"
# Unsaved text written outside as is: the tab is clean.
python3 -c 'import sys; d = open(sys.argv[1], "rb").read(); open(sys.argv[2], "wb").write(d[:-1] + b"S\n" if d.endswith(b"\n") else d + b"S")' "$doc" "$work/typed"
cp "$doc" "$work/saved"
changed=$(count "changed outside")
now S
cp "$work/typed" "$doc"
more "changed outside" "$changed"
sleep 2
clean=$(count "unsaved false")
printf 'gamma\n' >>"$doc"
more "unsaved false" "$clean"
sleep 1
grep -q gamma "$doc" || fail "the outside change was not taken"
changed=$(count "changed outside")
cp "$work/saved" "$doc"
more "changed outside" "$changed"
# A save that meets the same text keeps saving after.
python3 -c 'import sys; d = open(sys.argv[1], "rb").read(); open(sys.argv[2], "wb").write(d[:-1] + b"T\n" if d.endswith(b"\n") else d + b"T")' "$doc" "$work/same"
before=$(count autosaves)
now T
for _ in $(seq 200); do [ "$(count autosaves)" -gt "$before" ] && break; sleep 0.01; done
cp "$work/same" "$doc"
sleep 2
before=$(count autosaves)
now U
autosaved "$before"
grep -q TU "$doc" || fail "the next edit after a same-text save was not saved: $(tr '\n' '/' <"$doc")"
before=$(count autosaves)
key 'key code 51'
key 'key code 51'
autosaved "$before"
# 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 ": unsaved text from .* is back")" -ge 2 ] && break; sleep 0.25; done
[ "$(count ": unsaved text from .* is back")" = 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 ": unsaved text from .* is back")" = 2 ] || fail "after a discard: $(grep ": unsaved text from .* is back" "$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=
# A clean tab with the disk's text closes without asking.
clean=$work/notes/c.md
printf 'clean\n' >"$clean"
launch "$clean"
waits 'mark: shown as "visual"'
changes=$(count "unsaved false")
printf 'outside\n' >"$clean.tmp"
mv "$clean.tmp" "$clean"
more "unsaved false" "$changes"
sleep 1
[ "$("$work/ax-press" "$app" Close AXButton)" = pressed ] || fail "no tab Close"
sleep 2
! seen "Don't Save" || fail "a clean tab that took the disk's text asked to save"
kill "$app"
wait "$app" 2>/dev/null || true
app=
echo "ok: mark outside"
-188
View File
@@ -1,188 +0,0 @@
#!/usr/bin/env bash
# MARK-05, MARK-10 live: folder tree, outline, search, recent.
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 click; do
swiftc -O -o "$work/$tool" "$root/tests/scripts/$tool.swift"
done
mkdir -p "$work/notes/sub"
# A heading inside a quote Milkdown rewrites: one raw block.
printf '# Alpha\n\nSome text.\n\n> ## Beta ##\n>\n> More words here.\n' >"$work/notes/a.md"
# A hit hidden in a link's address comes first.
printf '# Other\n\nwords [link](words)\nwords too\n' >"$work/notes/sub/b.md"
printf 'banana\nana too\n\n[link](words)\nwords two' >"$work/notes/d.md"
printf -- '---\ntitle: T\n---\n# Heading\n\nneedle here.\n\n# Other\n\nother paragraph.\n' >"$work/notes/c.md"
printf 'words\n' >"$work/notes/skip.txt"
# A file the search cannot read.
printf 'words\n' >"$work/notes/bad.md"
chmod 000 "$work/notes/bad.md"
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() {
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)"
}
# Sets x and y to element `$1`'s center.
at() {
local found
for _ in $(seq 40); do
found=$("$work/ax-values" "$app" | awk -F'\t' -v want="title=$1" '$1 != "AXMenuItem" && $3 == want { sub("at=", "", $5); print $5; exit }')
if [ -n "$found" ]; then
IFS=, read -r x y <<<"$found"
return 0
fi
sleep 0.25
done
fail "no '$1' in the AX tree"
}
key() { osascript -e "tell application \"System Events\" to $1" -e 'delay 0.8' >/dev/null; }
press() { [ "$("$work/ax-press" "$app" "$1")" = pressed ] || fail "could not press $1"; sleep 0.8; }
saves() {
local before
before=$(grep -c "office: saved" "$work/err" || true)
key 'keystroke "s" using command down'
for _ in $(seq 40); do
[ "$(grep -c "office: saved" "$work/err" || true)" -gt "$before" ] && return 0
sleep 0.25
done
fail "no save"
}
launch "$work/notes" "$work/notes/a.md"
waits 'mark: shown as'
[ "$("$work/ax-press" $$ none)" != untrusted ] || fail "needs Accessibility permission"
osascript -e "tell application \"System Events\" to set frontmost of (first process whose unix id is $app) to true" -e 'delay 1' >/dev/null
# Tree rows are tree items; Return opens a picked one.
at b.md
"$work/click" "$x" "$y"
key 'key code 36'
waits "opened b.md"
# An outline heading takes the caret there.
press a.md
press Outline
at Outline
"$work/click" "$((x - 64))" "$((y + 64))"
key 'keystroke "Z"'
saves
grep -qx '> ## ZBeta ##' "$work/notes/a.md" || fail "outline jump: $(cat "$work/notes/a.md")"
# Folder search finds lines; a hit opens there.
press Search
at Folder
"$work/click" "$x" "$y"
press "This file"
"$work/click" "$x" "$((y - 37))"
key 'keystroke "words"'
key 'key code 36'
press Folder
at "> More words here."
at "words too"
at "bad.md was not searched"
! "$work/ax-values" "$app" | grep -q "title=words " || fail "a .txt file was searched"
chmod 600 "$work/notes/bad.md"
# Inside a paragraph the caret lands on the hit's line.
press "words too"
key 'key code 36'
key 'keystroke "Q"'
saves
[ "$(cat "$work/notes/sub/b.md")" = $'# Other\n\nwords [link](words)\nQwords too' ] || fail "search hit: $(cat "$work/notes/sub/b.md")"
# Lines count from the file's top, past front matter.
press Search
at Folder
"$work/click" "$x" "$((y - 37))"
key 'keystroke "a" using command down'
key 'keystroke "needle"'
key 'key code 36'
at "needle here."
press "needle here."
key 'key code 36'
waits "opened c.md"
sleep 1
key 'keystroke "Q"'
saves
grep -qx 'Qneedle here.' "$work/notes/c.md" || fail "front matter offset: $(cat "$work/notes/c.md")"
# This file's results follow the shown tab.
press Search
press "This file"
at "Qneedle here."
press b.md
sleep 1.5
! "$work/ax-values" "$app" | grep -q "title=Qneedle here." || fail "results stayed with the last tab"
# Overlapping hits; a hit hidden in a link's address.
press Search
at Folder
"$work/click" "$x" "$((y - 37))"
key 'keystroke "a" using command down'
key 'keystroke "ana"'
key 'key code 36'
press Folder
press "ana too"
key 'key code 36'
waits "opened d.md"
sleep 1
key 'keystroke "Q"'
saves
[ "$(cat "$work/notes/d.md")" = $'banana\nQana too\n\n[link](words)\nwords two' ] || fail "overlapping hit: $(cat "$work/notes/d.md")"
press Search
at Folder
"$work/click" "$x" "$((y - 37))"
key 'keystroke "a" using command down'
key 'keystroke "words"'
key 'key code 36'
press Folder
press "[link](words)"
key 'key code 36'
sleep 1
key 'keystroke "R"'
saves
[ "$(cat "$work/notes/d.md")" = $'banana\nQana too\n\n[Rlink](words)\nwords two' ] || fail "hidden hit: $(cat "$work/notes/d.md")"
# A rename shows in the folder's tree.
press Files
key 'key code 120'
key 'keystroke "a" using command down'
key 'keystroke "moved.md"'
key 'key code 36'
waits "renamed"
at moved.md
! "$work/ax-values" "$app" | grep -q "^AXRow.*title=d.md " || fail "the tree kept the old name"
kill "$app"
wait "$app" 2>/dev/null || true
app=
# The recent list outlives the app.
python3 -c 'import json,sys; l=json.load(open(sys.argv[1])); sys.exit(0 if any(p.endswith("notes") for p in l) and any(p.endswith("a.md") for p in l) else 1)' \
"$(find "$work/Library" -name recent.json)" || fail "recent: $(cat "$(find "$work/Library" -name recent.json)")"
launch
waits 'ElyMark window open'
at notes
press notes
key 'key code 36'
waits "opens folder"
at Outline
kill "$app"
wait "$app" 2>/dev/null || true
app=
! grep -q "panicked" "$work/err" || fail "panics: $(grep -m3 panicked "$work/err")"
echo "ok: mark places"
-180
View File
@@ -1,180 +0,0 @@
#!/usr/bin/env bash
# QA-21: a save keeps bytes no edit touched.
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; chmod -R u+rw "$work"; 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"
swiftc -O -o "$work/press" "$root/tests/scripts/ax-press.swift"
waits() {
for _ in $(seq 200); do
grep -qF "$1" "$work/err" && return 0
kill -0 "$app" 2>/dev/null || break
sleep 0.25
done
fail "never saw '$1': $(grep -E 'mark|office' "$work/err" | tail -n 5)"
}
# Starts ElyMark on `file`; it must show visually.
launch() {
: >"$work/err"
ELY_LOG=info ELY_LOCALE=en ELY_NO_DIALOG=1 HOME="$work" XDG_DATA_HOME="$work/data" \
"$root/target/debug/ely-mark" "$1" >"$work/out" 2>"$work/err" &
app=$!
}
shown() {
waits "mark: shown as"
grep -qF 'mark: shown as "visual", fallback false' "$work/err" ||
fail "not shown visually: $(grep 'mark: shown' "$work/err")"
osascript -e "tell application \"System Events\" to set frontmost of (first process whose unix id is $app) to true" \
-e 'delay 0.5' >/dev/null
}
type() { osascript -e "tell application \"System Events\" to $1" -e 'delay 1' >/dev/null; }
save_quit() {
type 'keystroke "s" using command down'
waits "office: saved"
osascript -e 'tell application "System Events" to keystroke "q" using command down' >/dev/null
for _ in $(seq 40); do
kill -0 "$app" 2>/dev/null || break
sleep 0.25
done
! kill -0 "$app" 2>/dev/null || fail "ElyMark did not quit"
app=
! grep -q "panicked" "$work/err" || fail "panics: $(grep -m3 panicked "$work/err")"
}
# Saves `name` after `keys`; it must equal `want`.
check() {
local name=$1 text=$2 keys=$3 want=$4
printf '%s' "$text" >"$work/$name"
launch "$work/$name"
shown
[ -z "$keys" ] || type "$keys"
save_quit
[ "$(cat "$work/$name"; echo .)" = "$want." ] || fail "$name saved as: $(cat "$work/$name")"
}
# BOM, CRLF, front matter and syntax Milkdown rewrites.
python3 - "$work/keep.md" <<'PY'
import sys
text = """---
title: Keep
tags: [a, b]
---
First paragraph.
Setext
======
1) paren list
2) second
* star
* list
<div align="center">
<b>html</b>
</div>
|a|b|
|-|-|
|1|2|
See [[Wiki Link]], ~sub~ and [a ref][r].
next line
[r]: https://example.com "T"
Last paragraph.
"""
open(sys.argv[1], "wb").write(b"\xef\xbb\xbf" + text.replace("\n", "\r\n").encode())
PY
cp "$work/keep.md" "$work/plain.md"
launch "$work/plain.md"
shown
save_quit
cmp -s "$work/keep.md" "$work/plain.md" || fail "an unedited save changed bytes"
cp "$work/keep.md" "$work/edited.md"
launch "$work/edited.md"
shown
type 'keystroke "x"'
save_quit
python3 - "$work/keep.md" "$work/edited.md" <<'PY'
import sys
before, after = (open(path, "rb").read() for path in sys.argv[1:])
want = before.replace(b"First paragraph.", b"xFirst paragraph.", 1)
if after != want:
sys.exit(f"FAIL: the edit rewrote more than its block:\n{after!r}")
PY
# Blocks Milkdown would rewrite are edited as raw text.
check wiki.md $'Alpha with [[Wiki Link]].\n' 'keystroke "x"' $'xAlpha with [[Wiki Link]].\n'
quote=$'> First.\n>\n> Setext\n> ======\n>\n> Last with [[Wiki]].\n'
check quote.md "$quote" 'keystroke "x"' "x$quote"
check defs.md $'[r]: https://example.com\n' 'keystroke "x"' $'x[r]: https://example.com\n'
# A compact table stays a table; edits keep its style.
check table.md $'|a|b|\n|-|-|\n|1|2|\n' 'keystroke "x"' $'|xa|b|\n|-|-|\n|1|2|\n'
check dashes.md $'| a | b |\n|---|---|\n| 1 | 2 |\n' 'keystroke "x"' $'| xa | b |\n| - | - |\n| 1 | 2 |\n'
check quoted.md $'> |alpha|beta|\n> |---|---|\n> |1|2|\n' 'keystroke "x"' $'> |xalpha|beta|\n> |-|-|\n> |1|2|\n'
check listed.md $'- item\n\n |alpha|beta|\n |---|---|\n |1|2|\n' 'keystroke "x"' $'- xitem\n\n |alpha|beta|\n |-|-|\n |1|2|\n'
# Items open with a paragraph; a leading table stays raw.
check leading.md $'- |alpha|beta|\n |---|---|\n' 'keystroke "x"' $'x- |alpha|beta|\n |---|---|\n'
check short.md $'|a|b|c|\n|-|-|-|\n|1|2|\n' 'keystroke "x"' $'|xa|b|c|\n|-|-|-|\n|1|2||\n'
# Cells Milkdown would rewrite keep the table raw.
check wikicell.md $'|a|[[Wiki Link]]|\n|-|-|\n|1|2|\n' 'keystroke "x"' $'x|a|[[Wiki Link]]|\n|-|-|\n|1|2|\n'
# Deleting forward at a hidden definition moves nothing.
gap=$'Alpha.\n\n[r]: /path " T " \n\nBeta.\n\nTail.\n'
check gap.md "$gap" 'key code 119' "$gap"
printf '%s' "$gap" >"$work/gap2.md"
launch "$work/gap2.md"
shown
type 'key code 119'
type 'key code 117'
save_quit
[ "$(cat "$work/gap2.md"; echo .)" = "$gap." ] || fail "the definition moved: $(cat "$work/gap2.md")"
# A reloaded page's edits still save.
printf 'Alpha.\n' >"$work/crash.md"
launch "$work/crash.md"
shown
# Two versions first: the new page counts from one.
type 'keystroke "x"'
type 'keystroke "z"'
pkill -P "$app" -f -- "--type=renderer" || fail "no renderer to stop"
pressed=
for _ in $(seq 40); do
[ "$("$work/press" "$app" Reload)" = pressed ] && pressed=1 && break
sleep 0.25
done
[ -n "$pressed" ] || fail "no Reload button"
for _ in $(seq 80); do
[ "$(grep -c 'mark: shown as' "$work/err")" -ge 2 ] && break
sleep 0.25
done
osascript -e "tell application \"System Events\" to set frontmost of (first process whose unix id is $app) to true" >/dev/null
type 'keystroke "y"'
save_quit
grep -q "y" "$work/crash.md" || fail "an edit after reload was lost: $(cat "$work/crash.md")"
# An unreadable file says so; no password is asked.
printf 'secret\n' >"$work/denied.md"
chmod 000 "$work/denied.md"
launch "$work/denied.md"
waits "did not open"
grep -q "Permission denied" "$work/err" || fail "the reason is missing: $(grep 'did not open' "$work/err")"
! grep -q "locked" "$work/err" || fail "taken for a locked file"
kill "$app"
app=
echo "ok: mark session"
-223
View File
@@ -1,223 +0,0 @@
#!/usr/bin/env bash
# MARK-06, MARK-07 live: find, replace, Tab, lists, tables, images.
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 click drop; do
swiftc -O -o "$work/$tool" "$root/tests/scripts/$tool.swift"
done
mkdir "$work/notes"
printf 'alpha beta alpha\n\ngamma alpha\n' >"$work/notes/t.md"
# A two-pixel PNG.
python3 -c 'import struct,zlib,sys
c=lambda t,d: struct.pack(">I",len(d))+t+d+struct.pack(">I",zlib.crc32(t+d))
open(sys.argv[1],"wb").write(b"\x89PNG\r\n\x1a\n"+c(b"IHDR",struct.pack(">IIBBBBB",2,2,8,2,0,0,0))+c(b"IDAT",zlib.compress(b"\0"+b"\xff\0\0"*2+b"\0"+b"\0\xff\0"*2))+c(b"IEND",b""))' "$work/red pic.png"
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() {
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)"
}
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"
}
# Clicks the find box's `$1`th text field, top first.
field() {
local found
found=$("$work/ax-values" "$app" | awk -F'\t' '$1 == "AXTextField" { sub("at=", "", $5); print $5 }' | sort -t, -k2 -n | sed -n "$1p")
[ -n "$found" ] || fail "no find field $1"
IFS=, read -r x y <<<"$found"
"$work/click" "$x" "$y"
sleep 0.5
}
saves() {
local before
before=$(grep -c "office: saved" "$work/err" || true)
key 'keystroke "s" using command down'
for _ in $(seq 40); do
[ "$(grep -c "office: saved" "$work/err" || true)" -gt "$before" ] && return 0
sleep 0.25
done
fail "no save"
}
is() { [ "$(cat "$work/notes/t.md")" = "$1" ] || fail "$2: $(cat "$work/notes/t.md")"; }
launch "$work/notes/t.md"
waits 'mark: shown as "visual"'
[ "$("$work/ax-press" $$ none)" != untrusted ] || fail "needs Accessibility permission"
osascript -e "tell application \"System Events\" to set frontmost of (first process whose unix id is $app) to true" -e 'delay 2' >/dev/null
# Replace all in the visual editor.
key 'keystroke "f" using command down'
key 'keystroke "alpha"'
press "Toggle replace"
field 2
key 'keystroke "omega"'
press "Replace all"
saves
is $'omega beta omega\n\ngamma omega' "replace all"
# A regular expression in the source editor.
press Source
waits 'mark: shown as "source"'
field 1
key 'keystroke "a" using command down'
key 'keystroke "g.mma"'
press "Regular expression"
field 2
key 'keystroke "a" using command down'
key 'keystroke "delta"'
press Replace
saves
is $'omega beta omega\n\ndelta omega' "regex replace"
# Tab indents the selected line; Esc closes the box first.
field 1
key 'key code 53'
key 'key code 48'
saves
grep -qx '[[:space:]]\{1,\}delta omega' "$work/notes/t.md" || fail "tab: $(cat "$work/notes/t.md")"
key 'key code 48 using shift down'
saves
is $'omega beta omega\n\ndelta omega' "shift-tab"
# A list item continues on Return.
key 'key code 125 using command down'
key 'key code 36'
key 'keystroke "- one"'
key 'key code 36'
key 'keystroke "two"'
saves
grep -qx -- '- two' "$work/notes/t.md" || fail "list: $(cat "$work/notes/t.md")"
# Tables: insert, then a column and a row.
press Visual
waits 'mark: shown as "visual"'
# Closing the find box hands the keys to the page.
key 'keystroke "f" using command down'
key 'key code 53'
# Command arrows move though AppKit sends no edit command.
key 'key code 125 using command down'
press "Insert Table"
press "Add Column Right"
press "Add Row Below"
saves
header=$(grep -m1 '^ *|' "$work/notes/t.md" || true)
[ "$(tr -cd '|' <<<"$header" | wc -c | tr -d ' ')" = 5 ] || fail "table columns: $(cat "$work/notes/t.md")"
[ "$(grep -c '^ *|' "$work/notes/t.md")" = 5 ] || fail "table rows: $(cat "$work/notes/t.md")"
[ "$(head -n 1 "$work/notes/t.md")" = "omega beta omega" ] || fail "table not at the end: $(cat "$work/notes/t.md")"
press "Delete Column"
saves
header=$(grep -m1 '^ *|' "$work/notes/t.md" || true)
[ "$(tr -cd '|' <<<"$header" | wc -c | tr -d ' ')" = 4 ] || fail "delete column: $(cat "$work/notes/t.md")"
# A dropped image is copied beside the file, name escaped.
window=$("$work/ax-values" "$app" | awk -F'\t' '$1 == "AXWindow" { sub("at=", "", $5); print $5; exit }')
IFS=, read -r x y <<<"$window"
[ "$("$work/drop" "$work/red pic.png" "$x" "$y")" = dropped ] || fail "the drop was refused"
waits "mark: attached 1 images"
saves
cmp -s "$work/red pic.png" "$work/notes/assets/red pic.png" || fail "dropped copy: $(ls -R "$work/notes")"
grep -qF '](assets/red%20pic.png)' "$work/notes/t.md" || fail "drop link: $(cat "$work/notes/t.md")"
# A pasted image lands in assets beside the file.
if [ "${ELY_CLIPBOARD_TEST:-}" = 1 ]; then
osascript -e "set the clipboard to (read (POSIX file \"$work/red pic.png\") as «class PNGf»)"
key 'key code 125 using command down'
key 'key code 36'
key 'keystroke "v" using command down'
saves
png=$(ls "$work/notes/assets"/pasted-*.png 2>/dev/null | head -n 1)
[ -n "$png" ] || fail "no pasted image: $(ls -R "$work/notes")"
grep -qF "](assets/$(basename "$png"))" "$work/notes/t.md" || fail "image link: $(cat "$work/notes/t.md")"
fi
# Saved elsewhere mid-copy: no link to nowhere.
mkfifo "$work/slow.png"
mkdir "$work/elsewhere"
window=$("$work/ax-values" "$app" | awk -F'\t' '$1 == "AXWindow" { sub("at=", "", $5); print $5; exit }')
IFS=, read -r x y <<<"$window"
[ "$("$work/drop" "$work/slow.png" "$x" "$y")" = dropped ] || fail "the slow drop was refused"
press "Save As…"
key 'keystroke "g" using {command down, shift down}'
key "keystroke \"$work/elsewhere\""
key 'key code 36'
key 'key code 36'
waits "elsewhere/t.md"
(printf 'x' >"$work/slow.png" &)
waits "the file moved while images copied"
! grep -q "slow" "$work/elsewhere/t.md" || fail "a link to the old folder: $(cat "$work/elsewhere/t.md")"
kill "$app"
wait "$app" 2>/dev/null || true
app=
! grep -q "panicked" "$work/err" || fail "panics: $(grep -m3 panicked "$work/err")"
# Two tabs; typing a replacement keeps the hit.
printf 'alpha beta alpha beta alpha\n' >"$work/notes/v.md"
printf 'alpha beta\n' >"$work/notes/w.md"
cp "$work/red pic.png" "$work/a]b.png"
launch "$work/notes/v.md" "$work/notes/w.md"
waits 'mark: shown as "visual"'
osascript -e "tell application \"System Events\" to set frontmost of (first process whose unix id is $app) to true" -e 'delay 2' >/dev/null
press v.md
sleep 1
key 'keystroke "f" using command down'
key 'keystroke "alpha"'
press "Toggle replace"
field 2
key 'keystroke "XY"'
press Replace
saves
[ "$(cat "$work/notes/v.md")" = "XY beta alpha beta alpha" ] || fail "the hit moved: $(cat "$work/notes/v.md")"
# Each tab follows the box, not its last query.
press w.md
field 1
key 'keystroke "a" using command down'
key 'keystroke "beta"'
field 2
key 'keystroke "a" using command down'
key 'keystroke "Y"'
press v.md
field 1
key 'keystroke "a" using command down'
key 'keystroke "alpha"'
field 2
key 'keystroke "a" using command down'
key 'keystroke "Z"'
press w.md
press "Replace all"
saves
[ "$(cat "$work/notes/w.md")" = "Z beta" ] || fail "a stale query: $(cat "$work/notes/w.md")"
# Alt text in source closes no brackets.
press Source
waits 'mark: shown as "source"'
window=$("$work/ax-values" "$app" | awk -F'\t' '$1 == "AXWindow" { sub("at=", "", $5); print $5; exit }')
IFS=, read -r x y <<<"$window"
[ "$("$work/drop" "$work/a]b.png" "$x" "$y")" = dropped ] || fail "the bracket drop was refused"
waits "mark: attached 1 images"
saves
grep -qF '![a\]b](assets/a%5Db.png)' "$work/notes/w.md" || fail "alt escape: $(cat "$work/notes/w.md")"
kill "$app"
wait "$app" 2>/dev/null || true
app=
! grep -q "panicked" "$work/err" || fail "panics: $(grep -m3 panicked "$work/err")"
echo "ok: mark tools"