m14-foundation.sh
raw
#!/usr/bin/env bash
# M14 foundation test under quibble:
# 1. keyed `(for …)` rows grow through the compiled handler (stdout count
# plus a screenshot diff),
# 2. clearing swaps in the `(if …)` empty branch (screenshot diff),
# 3. Ctrl+D (an app shortcut) opens a modal dialog overlay,
# 4. Escape dismisses it (focused-widget → overlay routing),
# 5. reopening and clicking has no effect beneath the modal barrier,
# 6. idle renders zero frames (no focusables, no timers).
set -euo pipefail
cd "$(dirname "$0")/.."
cargo build -p guiduck --example m14_demo
workdir=$(mktemp -d)
sock="$workdir/quibble.sock"
cleanup() {
quibblectl --socket "$sock" quit >/dev/null 2>&1 || true
rm -rf "$workdir"
}
trap cleanup EXIT
GUIDUCK_BACKEND=software GUIDUCK_TRACE_FRAMES=1 \
quibble --socket "$sock" --width 360 --height 300 \
target/debug/examples/m14_demo \
>"$workdir/out.log" 2>"$workdir/err.log" &
for _ in $(seq 1 50); do
quibblectl --socket "$sock" ping >/dev/null 2>&1 && break
sleep 0.2
done
quibblectl --socket "$sock" wait-window --timeout-ms 30000 >/dev/null
sleep 1
frames() { grep -c '^guiduck-frame' "$workdir/err.log" || true; }
qc() { quibblectl --socket "$sock" "$@" >/dev/null; }
shot() { quibblectl --socket "$sock" screenshot -o "$workdir/$1" >/dev/null; }
last() { grep "^$1=" "$workdir/out.log" | tail -1; }
fail=0
KEY_ESC=1; KEY_LEFTCTRL=29; KEY_D=32
key() {
quibblectl --socket "$sock" key "$1" press >/dev/null
quibblectl --socket "$sock" key "$1" release >/dev/null
}
# Layout (padding 8, rows 20 high, gap 4): button centers.
ADD_X=48; ADD_Y=18
CLEAR_X=48; CLEAR_Y=42
# 1: adding grows the keyed list.
shot list0.png
qc pointer-move $ADD_X $ADD_Y
qc click
sleep 0.3
shot list1.png
v=$(last todos)
if [ "$v" = "todos=3" ] && ! cmp -s "$workdir/list0.png" "$workdir/list1.png"; then
echo "keyed rows grew: $v"
else
echo "FAIL: add; got '$v'" >&2; fail=1
fi
# 1b: a per-row action — the first row's delete square (a `(remove
# todo.id)` invocation wire) drops exactly that row.
qc pointer-move 78 63
qc click
sleep 0.3
v=$(last todos)
if [ "$v" = "todos=2" ]; then
echo "per-row delete: $v"
else
echo "FAIL: row delete; got '$v'" >&2; fail=1
fi
# 2: clearing swaps in the empty-state branch.
qc pointer-move $CLEAR_X $CLEAR_Y
qc click
sleep 0.3
shot cleared.png
v=$(last todos)
if [ "$v" = "todos=0" ] && ! cmp -s "$workdir/list1.png" "$workdir/cleared.png"; then
echo "if-branch swapped in: $v"
else
echo "FAIL: clear; got '$v'" >&2; fail=1
fi
# 3: the Ctrl+D accelerator opens a modal dialog.
quibblectl --socket "$sock" key $KEY_LEFTCTRL press >/dev/null
key $KEY_D
quibblectl --socket "$sock" key $KEY_LEFTCTRL release >/dev/null
sleep 0.3
shot dialog.png
v=$(last dialog)
if [ "$v" = "dialog=open" ] && ! cmp -s "$workdir/cleared.png" "$workdir/dialog.png"; then
echo "shortcut opened the dialog"
else
echo "FAIL: Ctrl+D; got '$v'" >&2; fail=1
fi
# 4: clicks beneath the modal barrier do nothing (the add button is
# covered by the barrier, not the dialog).
qc pointer-move $ADD_X $ADD_Y
qc click
sleep 0.3
v=$(last todos)
if [ "$v" = "todos=0" ]; then
echo "modal barrier holds"
else
echo "FAIL: click pierced the modal; got '$v'" >&2; fail=1
fi
# 5: Escape dismisses it.
key $KEY_ESC
sleep 0.3
v=$(last dialog)
shot after.png
if [ "$v" = "dialog=closed" ] && ! cmp -s "$workdir/dialog.png" "$workdir/after.png"; then
echo "Escape dismissed the dialog"
else
echo "FAIL: Escape; got '$v'" >&2; fail=1
fi
# 6: idle (no focusables, no timers → strict zero).
q0=$(frames); sleep 3; q1=$(frames)
echo "idle frames: $q0 -> $q1"
[ "$q0" = "$q1" ] || { echo "FAIL: rendering while idle" >&2; fail=1; }
cp "$workdir/dialog.png" target/m14-dialog.png 2>/dev/null || true
[ "$fail" -eq 0 ] && echo "M14 foundation test PASSED"
exit $fail