Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Binary file added .github/assets/calculator-dark.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/assets/calculator-light.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/assets/notes-dark.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/assets/notes-light.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/assets/soundboard-dark.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/assets/soundboard-light.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
121 changes: 121 additions & 0 deletions .github/scripts/linux-canvas-smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# Linux canvas smoke under Xvfb.
#
# Exercises the Linux gpu_surface software path against system WebKitGTK
# without a display server: builds examples/ui-inbox with -Dplatform=linux
# -Dweb-engine=system -Dautomation=true, runs it under Xvfb, and asserts
# against the automation snapshot:
#
# 1. snapshot ready=true (app booted, automation server live)
# 2. gpu_backend=software (the software present path is active)
# 3. gpu_nonblank=true (real pixels were presented)
# 4. widget-click "Add task" -> '4 open' (automation input mutates state)
# 5. automate screenshot renders a non-empty PNG
#
# Deliberately NOT `set -e` (same as windows-canvas-smoke.sh): grep exits 1
# on zero matches, and under `set -e` an assignment like `x=$(grep ...)` or
# a swallowed `$(cli 2>&1)` capture dies with NO output — this job failed
# three times with nothing in the log but the exit code. Every assertion
# goes through fail(), which dumps the snapshot and the app log.
set -u

# WebKitGTK's bubblewrap sandbox needs unprivileged user namespaces, which
# ubuntu-24.04 runners restrict via AppArmor; without this the web process
# dies launching xdg-dbus-proxy and the app never publishes an automation
# snapshot (reproduced in a local container: ready=true never lands; with
# the sandbox disabled the full smoke passes). The sandbox is not what
# this smoke tests.
export WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS="${WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS:-1}"

# GTK_A11Y=none: under Xvfb there is no session bus providing org.a11y.Bus,
# and GTK4's a11y init blocks ~25 s on the GDBus name lookup before warning
# and continuing — the app's first runtime event landed after the readiness
# window had already expired (reproduced in a local container: without this
# the wait times out at startup; with it the full smoke passes).
# Accessibility is not what this smoke tests.
export GTK_A11Y="${GTK_A11Y:-none}"

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
app_dir="$repo_root/examples/ui-inbox"
snap="$app_dir/.zig-cache/native-sdk-automation/snapshot.txt"
cli="$repo_root/zig-out/bin/native"
app_log="${TMPDIR:-/tmp}/linux-canvas-smoke-app.log"

# Readiness budget. Even with GTK_A11Y=none, shared ubuntu-24.04 runners
# show a consistent ~27 s stall between EGL init and the app's first
# runtime event (measured in runs 28690855597 pass / 28691951139 fail —
# the SAME stall in both; the old hard 30 s `automate wait` window flipped
# green/red on one or two seconds of runner noise). Local containers show
# no stall at all. Widen the budget here instead of weakening the CLI
# default; every correctness assertion stays strict.
ready_timeout_ms=90000

app_pid=""
cleanup() {
[ -n "$app_pid" ] && kill "$app_pid" >/dev/null 2>&1
# xvfb-run does not forward signals to an already-detached app; reap the
# app and its Xvfb directly so local runs exit clean (CI would otherwise
# rely on the runner's orphan sweep).
pkill -f "$app_dir/zig-out/bin/ui-inbox" >/dev/null 2>&1
}
trap cleanup EXIT

diagnostics() {
echo "---- diagnostics ----"
echo "-- snapshot ($snap):"
if [ -f "$snap" ]; then tr '|' '\n' < "$snap" | sed 's/^/ /'; else echo " (missing)"; fi
echo "-- app log head ($app_log):"
head -20 "$app_log" 2>/dev/null | sed 's/^/ /'
echo "-- app log tail ($app_log):"
tail -40 "$app_log" 2>/dev/null | sed 's/^/ /'
echo "---------------------"
}

fail() {
echo "FAIL: $1"
diagnostics
exit 1
}

# ---- build ----------------------------------------------------------------
(cd "$repo_root" && zig build) || fail "root zig build (CLI) failed"
(cd "$app_dir" && zig build -Dplatform=linux -Dweb-engine=system -Dautomation=true) \
|| fail "ui-inbox Linux build failed"

# ---- launch ---------------------------------------------------------------
cd "$app_dir" || fail "missing $app_dir"
rm -rf .zig-cache/native-sdk-automation
xvfb-run -a "$app_dir/zig-out/bin/ui-inbox" > "$app_log" 2>&1 &
app_pid=$!

# ---- 1: automation snapshot becomes ready ---------------------------------
# `automate assert` self-reports on timeout (missing patterns + snapshot
# tail) and prints the measured latency on success, so green logs carry
# the readiness margin.
"$cli" automate assert --timeout-ms "$ready_timeout_ms" 'ready=true' \
|| fail "snapshot never became ready"

# ---- 2 + 3: software backend presented non-blank pixels --------------------
"$cli" automate assert --timeout-ms 30000 'gpu_nonblank=true' \
|| fail "gpu_nonblank never became true"
grep -q 'gpu_backend=software' "$snap" || fail "gpu_backend is not software"
echo "== canvas: $(grep -o 'gpu_backend=[a-z]*' "$snap" | head -1)" \
"$(grep -o 'gpu_nonblank=[a-z]*' "$snap" | head -1)"

# ---- 4: automation widget-click mutates the model --------------------------
echo "== open before click: $(grep -oE '[0-9]+ open' "$snap" | head -1)"
add_id=$(grep -o 'widget @w1/inbox-canvas#[0-9]* role=button name="Add task"' "$snap" \
| grep -o '#[0-9]*' | tr -d '#')
[ -n "$add_id" ] || fail "Add task button not found in snapshot"
"$cli" automate widget-click inbox-canvas "$add_id" || fail "CLI widget-click failed"
"$cli" automate assert --timeout-ms 30000 '4 open' \
|| fail "widget-click did not reach '4 open'"
echo "== open after click: $(grep -oE '[0-9]+ open' "$snap" | head -1)"

# ---- 5: screenshot renders a non-empty PNG ---------------------------------
"$cli" automate screenshot inbox-canvas || fail "CLI screenshot failed"
test -s .zig-cache/native-sdk-automation/screenshot-inbox-canvas.png \
|| fail "screenshot PNG missing or empty"

echo "PASS: linux canvas smoke"
exit 0
172 changes: 172 additions & 0 deletions .github/scripts/windows-canvas-smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
#!/usr/bin/env bash
# Windows canvas smoke under Wine.
#
# Exercises the Windows gpu_surface software path (src/platform/windows/
# webview2_host.cpp: child HWND, WM_TIMER frame events, SetDIBitsToDevice
# blits) without Windows hardware: cross-compiles examples/ui-inbox for
# x86_64-windows-gnu, runs the .exe under Xvfb + Wine, and asserts against
# the automation snapshot:
#
# 1. snapshot ready=true (app booted, automation server live)
# 2. gpu_backend=software (the SetDIBitsToDevice path is active)
# 3. gpu_nonblank=true (real pixels were presented)
# 4. widget-click "Add task" -> '4 open' (automation input mutates state)
# 5. real X11 click + typing lands in the draft textbox (XTEST -> Wine ->
# WM_LBUTTONDOWN/WM_CHAR -> runtime)
#
# Step 5 (xdotool) is deliberately included: it is the only coverage of the
# Win32 pointer/keyboard input mapping in webview2_host.cpp. It is also the
# flakiest step (window lookup, focus without a window manager), so every
# failure path dumps the X window list, the snapshot, and the app log.
#
# Deliberately NOT `set -e`: grep exits 1 on zero matches inside the poll
# loops, and we want explicit, diagnosable failures instead of silent early
# exits. Every assertion goes through fail(), which dumps diagnostics.
set -u

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
app_dir="$repo_root/examples/ui-inbox"
snap="$app_dir/.zig-cache/native-sdk-automation/snapshot.txt"
cli="$repo_root/zig-out/bin/native"
app_log="${TMPDIR:-/tmp}/windows-canvas-smoke-app.log"

# Wine needs an X display; when none is present (CI), re-exec the whole
# script under a private Xvfb server so the app and xdotool share it. The
# explicit screen size beats xvfb-run's 640x480x8 default: the app window is
# 720x520 and Wine wants a 24-bit visual.
if [ -z "${DISPLAY:-}" ]; then
exec xvfb-run -a --server-args="-screen 0 1280x800x24" "$0" "$@"
fi

export WINEPREFIX="${WINEPREFIX:-$repo_root/.zig-cache/wineprefix}"
export WINEDEBUG="${WINEDEBUG:--all}"

app_pid=""
cleanup() {
[ -n "$app_pid" ] && kill "$app_pid" >/dev/null 2>&1
wineserver -k >/dev/null 2>&1
}
trap cleanup EXIT

diagnostics() {
echo "---- diagnostics ----"
echo "-- X windows:"
xdotool search --name "." 2>/dev/null | while read -r w; do
echo " $w: $(xdotool getwindowname "$w" 2>/dev/null)"
done
echo "-- snapshot ($snap):"
if [ -f "$snap" ]; then tr '|' '\n' < "$snap" | sed 's/^/ /'; else echo " (missing)"; fi
echo "-- app log tail ($app_log):"
tail -40 "$app_log" 2>/dev/null | sed 's/^/ /'
echo "---------------------"
}

fail() {
echo "FAIL: $1"
diagnostics
exit 1
}

# poll <seconds> <pattern>: wait until $snap contains <pattern>.
poll() {
local deadline=$((SECONDS + $1))
while [ "$SECONDS" -lt "$deadline" ]; do
[ -f "$snap" ] && grep -q "$2" "$snap" && return 0
sleep 0.5
done
return 1
}

# ---- build ----------------------------------------------------------------
(cd "$repo_root" && zig build) || fail "root zig build (CLI) failed"
(cd "$app_dir" && zig build -Dtarget=x86_64-windows-gnu -Dplatform=windows -Dweb-engine=system -Dautomation=true) \
|| fail "ui-inbox Windows cross-compile failed"

# ---- wineprefix -----------------------------------------------------------
# First run initializes the prefix (measured ~10-30s on CI-class machines);
# subsequent runs are instant, so no cache step is needed.
start=$SECONDS
wineboot --init >/dev/null 2>&1
wineserver --wait >/dev/null 2>&1
echo "== wineprefix ready in $((SECONDS - start))s ($WINEPREFIX)"

# ---- launch ---------------------------------------------------------------
cd "$app_dir" || fail "missing $app_dir"
rm -rf .zig-cache/native-sdk-automation
mkdir -p .zig-cache/native-sdk-automation
wine zig-out/bin/ui-inbox.exe > "$app_log" 2>&1 &
app_pid=$!

# ---- 1: automation snapshot becomes ready ---------------------------------
poll 180 'ready=true' || fail "snapshot never became ready"
echo "== ready: $(head -1 "$snap" | cut -d'|' -f1)"

# ---- 2 + 3: software backend presented non-blank pixels --------------------
poll 60 'gpu_nonblank=true' || fail "gpu_nonblank never became true"
grep -q 'gpu_backend=software' "$snap" || fail "gpu_backend is not software"
echo "== canvas: $(grep -o 'gpu_backend=[a-z]*' "$snap" | head -1)" \
"$(grep -o 'gpu_nonblank=[a-z]*' "$snap" | head -1)" \
"$(grep -o 'gpu_sample=0x[0-9a-f]*' "$snap" | head -1)" \
"$(grep -o 'gpu_present_mode=[a-z]*' "$snap" | head -1)"

# ---- 4: automation widget-click mutates the model --------------------------
echo "== open before click: $(grep -oE '[0-9]+ open' "$snap" | head -1)"
add_id=$(grep -o 'widget @w1/inbox-canvas#[0-9]* role=button name="Add task"' "$snap" \
| grep -o '#[0-9]*' | tr -d '#')
[ -n "$add_id" ] || fail "Add task button not found in snapshot"
"$cli" automate widget-click inbox-canvas "$add_id" || fail "CLI widget-click failed"
poll 30 '4 open' || fail "widget-click did not reach '4 open'"
echo "== open after click: $(grep -oE '[0-9]+ open' "$snap" | head -1)"

# ---- 5: real X11 input through the Win32 path ------------------------------
# Root-coordinate math. Hidden-titlebar windows keep the full overlapped
# frame and reclaim the caption band through WM_NCCALCSIZE, so the Win32
# client area starts at the very top of the window. Under a WM-less Wine
# the X11 driver still places (and reports) the client X window at the
# DEFAULT frame offset - one caption band lower - so the X origin sits a
# band BELOW where Win32 client coordinates actually map, and the reported
# X height is short by exactly that band (measured: X window 718x489 at
# y=30 for a 718x519 client whose clicks land at y=0). The snapshot knows
# the true client height, so the height shortfall IS the y correction; a
# standard-frame window reports matching heights and corrects by zero.
win=""
for w in $(xdotool search --name "." 2>/dev/null); do
case "$(xdotool getwindowname "$w" 2>/dev/null)" in
*[Ii]nbox*) win="$w" ;;
esac
done
[ -n "$win" ] || fail "app X window not found"
eval "$(xdotool getwindowgeometry --shell "$win")"
client_h=$(grep -o 'window @w1 "[^"]*" bounds=([^)]*)' "$snap" | head -1 \
| sed -n 's/.*x\([0-9]*\)[^x]*$/\1/p')
[ -n "$client_h" ] || client_h=$HEIGHT
y_off=$((client_h - HEIGHT))
[ "$y_off" -ge 0 ] 2>/dev/null || y_off=0
echo "== x window $win: pos=($X,$Y) size=${WIDTH}x${HEIGHT} client_h=$client_h y_off=$y_off"
xdotool windowactivate "$win" >/dev/null 2>&1 || xdotool windowfocus "$win" >/dev/null 2>&1

draft_line=$(grep -o 'widget @w1/inbox-canvas#[0-9]* role=textbox[^|]*' "$snap" | head -1)
[ -n "$draft_line" ] || fail "draft textbox not found in snapshot"
bounds=$(echo "$draft_line" | grep -o 'bounds=([^)]*)')
bx=$(echo "$bounds" | sed -n 's/bounds=(\([0-9.]*\),.*/\1/p')
by=$(echo "$bounds" | sed -n 's/bounds=([0-9.]*,\([0-9.]*\) .*/\1/p')
bw=$(echo "$bounds" | sed -n 's/.* \([0-9.]*\)x[0-9.]*).*/\1/p')
bh=$(echo "$bounds" | sed -n 's/.* [0-9.]*x\([0-9.]*\)).*/\1/p')
[ -n "$bx" ] && [ -n "$by" ] && [ -n "$bw" ] && [ -n "$bh" ] || fail "could not parse draft bounds: $draft_line"
cx=$(awk "BEGIN{printf \"%d\", $X + $bx + $bw / 2}")
cy=$(awk "BEGIN{printf \"%d\", $Y - $y_off + $by + $bh / 2}")
echo "== clicking draft field $bounds at ($cx,$cy)"
xdotool mousemove "$cx" "$cy" click 1
# The click must move widget focus into the textbox before any keys are
# sent: spaces in the typed string would otherwise activate whatever
# widget held focus (a button press adds a task and the real failure -
# input landing in the wrong widget - would read as missing text).
poll 10 'role=textbox[^|]*focused=true' || fail "draft textbox did not take focus from the click"
sleep 1
xdotool type --delay 120 "hi from wine"
poll 30 'hi from wine' || fail "typed text never appeared in the snapshot"
echo "== draft after typing: $(grep -o 'widget @w1/inbox-canvas#[0-9]* role=textbox[^|]*' "$snap" | head -1 | cut -c1-160)"
echo "== input latency: $(grep -o 'gpu_input_latency_ns=[0-9]*' "$snap" | head -1)"

echo "PASS: windows canvas smoke"
exit 0
Loading
Loading