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:
1 parent
540637cada
commit
1f2920020a
34 files changed
+342
-4144
No files matched your search
@@ -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')
|
||||
@@ -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)"
|
||||
@@ -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')
|
||||
@@ -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\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)"
|
||||
@@ -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.' '' '' '' "<img src='assets/pic.png'>" '' \
|
||||
"<img alt='a > b' src='assets/pic.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$' '' '' '' '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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user