mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-28 12:12:47 +00:00
* fix(app): dock the New workspace composer like a chat on phones Only the composer is bounded by the viewport; the setup fields sit above it in a bottom-anchored stack that moves up as the draft grows. The keyboard reservation is retained on every screen, so the composer keeps one height across keyboard transitions. Removes the per-screen reserve policy. * fix(app): share composer docking across mobile hosts Extract chat's retained capacity and platform keyboard motion so chat, workspace drafts, and New workspace share one sizing and dismissal boundary. Keep the native drivers and settled portal reconciliation unchanged. * fix(app): preserve overlays in centered composer docks Honor the optional overlay child in both layouts so changing dock placement does not silently discard part of its content. * test(app): resolve keyboard controller through its web entry The shared keyboard entry point makes stream tests load the provider. Keep its real ESM dependencies inside the Vitest resolver so CommonJS does not bypass the React Native web alias and parse native Flow sources. * fix(app): keep native chat scrolling responsive Place keyboard dismissal behind scrollable content so an idle Android Fabric JS responder cannot intercept native drags. Keep decorative form padding transparent to iOS hit testing and exercise idle native scrolling after submission from every composer host.
675 lines
27 KiB
Bash
Executable File
675 lines
27 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../../.." && pwd)"
|
|
STATE_DIR="${PASEO_COMPOSER_KEYBOARD_STATE_DIR:-${REPO_ROOT}/.dev/agent-device-composer-keyboard}"
|
|
ARTIFACTS_DIR="${REPO_ROOT}/.dev/agent-device-artifacts/composer-keyboard-android"
|
|
SESSION="${PASEO_COMPOSER_KEYBOARD_SESSION:-composer-keyboard-android}"
|
|
APP_ID="${PASEO_COMPOSER_KEYBOARD_APP_ID:-sh.paseo.debug}"
|
|
DEVICE="${PASEO_COMPOSER_KEYBOARD_DEVICE:-paseo-api35}"
|
|
HELPER_IME="com.callstack.agentdevice.imehelper/.TestInputMethodService"
|
|
GBOARD_IME="com.google.android.inputmethod.latin/com.android.inputmethod.latin.LatinIME"
|
|
ASSERT="${REPO_ROOT}/packages/app/e2e/mobile/composer-keyboard/assert-composer-keyboard.mjs"
|
|
STALL_HERMES="${REPO_ROOT}/packages/app/e2e/mobile/composer-keyboard/stall-hermes.mjs"
|
|
METRO_PORT="${PASEO_MOBILE_E2E_METRO_PORT:-8082}"
|
|
DAEMON_HOST="${PASEO_COMPOSER_KEYBOARD_DAEMON_HOST:-127.0.0.1:6770}"
|
|
DAEMON_HOME="${PASEO_COMPOSER_KEYBOARD_DAEMON_HOME:-${REPO_ROOT}/.dev/composer-e2e-home}"
|
|
SERVER_ID="${PASEO_COMPOSER_KEYBOARD_SERVER_ID:-}"
|
|
MESSAGE=$'keyboard invariant line one\nline two\nline three\nline four'
|
|
LONG_MESSAGE="$(node -e 'process.stdout.write(Array.from({ length: 180 }, (_, index) => `line${index + 1}`).join(" "))')"
|
|
BLANK_LINE_DRAFT="$(node -e 'process.stdout.write("\n".repeat(22) + "ddjdj")')"
|
|
AGENT_TITLE="Keyboard dismiss QA $(date +%s)"
|
|
|
|
if [[ -z "${SERVER_ID}" ]]; then
|
|
if [[ ! -f "${DAEMON_HOME}/server-id" ]]; then
|
|
echo "Missing ${DAEMON_HOME}/server-id; set PASEO_COMPOSER_KEYBOARD_SERVER_ID" >&2
|
|
exit 1
|
|
fi
|
|
SERVER_ID="$(<"${DAEMON_HOME}/server-id")"
|
|
fi
|
|
|
|
ad() {
|
|
AGENT_DEVICE_STATE_DIR="${STATE_DIR}" agent-device "$@" --session "${SESSION}"
|
|
}
|
|
|
|
capture_screen() {
|
|
local output_path="$1"
|
|
ad screenshot "${output_path}" >/dev/null
|
|
}
|
|
|
|
snapshot_json() {
|
|
local output_path="$1"
|
|
local detail="${2:---raw}"
|
|
# A transient missing application window is a failed capture, never evidence.
|
|
for attempt in 1 2 3; do
|
|
if ad snapshot "${detail}" --json >"${output_path}"; then return; fi
|
|
sleep 1
|
|
done
|
|
return 1
|
|
}
|
|
|
|
capture_ui_xml() {
|
|
local output_path="$1"
|
|
# The helper reads current accessibility bounds without uiautomator's global
|
|
# idle wait (retained screens can keep that wait busy). It leaves Gboard open.
|
|
# Serialize those same physical bounds for the existing XML assertions.
|
|
snapshot_json "${output_path}.json" --raw
|
|
node "${ASSERT}" snapshot-xml "${output_path}.json" "${output_path}"
|
|
}
|
|
|
|
clear_input() {
|
|
ad fill 'editable=true' "x" --settle
|
|
adb shell input keycombination 113 29
|
|
adb shell input keyevent 67
|
|
}
|
|
|
|
clear_focused_input_with_adb() {
|
|
adb shell input keycombination 113 29
|
|
adb shell input keyevent 67
|
|
}
|
|
|
|
wait_for_ime() {
|
|
local expected="$1"
|
|
local previous_top=""
|
|
local stable_count=0
|
|
# Gboard's first show after an IME switch can take several seconds on a loaded host.
|
|
for _ in $(seq 1 100); do
|
|
local window_state
|
|
window_state="$(adb shell dumpsys window)"
|
|
if [[ "${expected}" == "true" ]]; then
|
|
local ime_top
|
|
ime_top="$(printf '%s' "${window_state}" | sed -n 's/.*type=ime frame=\[0,\([0-9][0-9]*\)\].*visible=true.*/\1/p' | head -1)"
|
|
if [[ -n "${ime_top}" && "${ime_top}" -gt 0 ]]; then
|
|
if [[ "${ime_top}" == "${previous_top}" ]]; then
|
|
stable_count="$((stable_count + 1))"
|
|
else
|
|
stable_count=0
|
|
previous_top="${ime_top}"
|
|
fi
|
|
if [[ "${stable_count}" -ge 2 ]]; then
|
|
return
|
|
fi
|
|
fi
|
|
elif ! printf '%s' "${window_state}" | rg -q "type=ime .*visible=true"; then
|
|
# The window stops reporting visible when the hide animation starts. Android
|
|
# drops a show request made while it is still running, so let it finish
|
|
# before the next tap.
|
|
sleep 0.75
|
|
return
|
|
fi
|
|
sleep 0.1
|
|
done
|
|
echo "IME did not become visible=${expected}" >&2
|
|
exit 1
|
|
}
|
|
|
|
ime_is_visible() {
|
|
adb shell dumpsys window | rg -q "type=ime .*visible=true"
|
|
}
|
|
|
|
open_gboard() {
|
|
local x="$1" y="$2" height
|
|
adb shell ime set "${GBOARD_IME}" >/dev/null
|
|
sleep 1
|
|
if ime_is_visible; then
|
|
adb shell input keyevent BACK
|
|
wait_for_ime false
|
|
fi
|
|
# IME switching can restore focus before accessibility reports its new frame.
|
|
# Locate the editor for each tap instead of translating a previous snapshot:
|
|
# an old editor position can now be a background that correctly dismisses it.
|
|
snapshot_json "${ARTIFACTS_DIR}/keyboard-target.json" --raw
|
|
read -r x y height < <(node "${ASSERT}" rect "${ARTIFACTS_DIR}/keyboard-target.json" editable right)
|
|
adb shell input tap "${x}" "${y}"
|
|
wait_for_ime true
|
|
snapshot_json "${ARTIFACTS_DIR}/keyboard-target-open.json" --raw
|
|
read -r x y height < <(node "${ASSERT}" rect "${ARTIFACTS_DIR}/keyboard-target-open.json" editable right)
|
|
adb shell input tap "${x}" "${y}"
|
|
}
|
|
|
|
|
|
read_keyboard_shift() {
|
|
local ime_top
|
|
local navigation_top
|
|
ime_top="$(adb shell dumpsys window | sed -n 's/.*type=ime frame=\[0,\([0-9][0-9]*\)\].*visible=true.*/\1/p' | head -1)"
|
|
navigation_top="$(adb shell dumpsys window | sed -n 's/.*type=navigationBars frame=\[0,\([0-9][0-9]*\)\].*/\1/p' | head -1)"
|
|
printf '%s\n' "$((navigation_top - ime_top))"
|
|
}
|
|
|
|
read_ime_top() {
|
|
adb shell dumpsys window | sed -n \
|
|
's/.*type=ime frame=\[0,\([0-9][0-9]*\)\].*visible=true.*/\1/p' | head -1
|
|
}
|
|
|
|
cleanup() {
|
|
adb shell ime set "${HELPER_IME}" >/dev/null 2>&1 || true
|
|
AGENT_DEVICE_STATE_DIR="${STATE_DIR}" agent-device daemon stop --clean >/dev/null 2>&1 || true
|
|
}
|
|
|
|
trap cleanup EXIT INT TERM
|
|
cleanup
|
|
mkdir -p "${STATE_DIR}" "${ARTIFACTS_DIR}"
|
|
|
|
workspaces_json="$(env -u PASEO_CALLER_AGENT_ID -u PASEO_AGENT_ID -u PASEO_WORKSPACE_ID \
|
|
npm run --silent cli -- workspace ls --json --host "${DAEMON_HOST}")"
|
|
workspace_id="$(node -e '
|
|
const workspaces = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
|
|
const cwd = process.argv[1];
|
|
const workspace = workspaces.find((candidate) => candidate.cwd === cwd);
|
|
if (!workspace) process.exit(1);
|
|
process.stdout.write(workspace.workspaceId);
|
|
' "${REPO_ROOT}" <<<"${workspaces_json}")"
|
|
|
|
AGENT_DEVICE_STATE_DIR="${STATE_DIR}" agent-device open "${APP_ID}" \
|
|
--platform android \
|
|
--device "${DEVICE}" \
|
|
--session "${SESSION}"
|
|
adb shell am force-stop "${APP_ID}"
|
|
adb shell am start \
|
|
-a android.intent.action.VIEW \
|
|
-d "exp+voice-mobile://expo-development-client/?url=http%3A%2F%2F10.0.2.2%3A${METRO_PORT}" \
|
|
"${APP_ID}" >/dev/null
|
|
sleep 3
|
|
|
|
# Create the fixture while the client is connected so its directory replica observes the insert.
|
|
ad wait 'id="message-input-root"' 90000
|
|
run_json="$(env -u PASEO_CALLER_AGENT_ID -u PASEO_AGENT_ID -u PASEO_WORKSPACE_ID \
|
|
npm run --silent cli -- run "Exercise the composer keyboard invariant flow" \
|
|
--background \
|
|
--title "${AGENT_TITLE}" \
|
|
--provider mock \
|
|
--model e2e-fast-stream \
|
|
--workspace "${workspace_id}" \
|
|
--cwd "${REPO_ROOT}" \
|
|
--host "${DAEMON_HOST}" \
|
|
--json)"
|
|
agent_id="$(node -e '
|
|
const result = JSON.parse(require("node:fs").readFileSync(0, "utf8"));
|
|
process.stdout.write(result.agentId);
|
|
' <<<"${run_json}")"
|
|
sleep 3
|
|
|
|
adb shell am start \
|
|
-a android.intent.action.VIEW \
|
|
-d "paseo://h/${SERVER_ID}/agent/${agent_id}" \
|
|
"${APP_ID}" >/dev/null
|
|
sleep 5
|
|
# The development-client bootstrap can consume the first link while Expo Router is mounting.
|
|
# Deliver the target again after the root navigator is live.
|
|
adb shell am start \
|
|
-a android.intent.action.VIEW \
|
|
-d "paseo://h/${SERVER_ID}/agent/${agent_id}" \
|
|
"${APP_ID}" >/dev/null
|
|
ad wait "text=\"${AGENT_TITLE}\"" 45000
|
|
ad wait 'editable=true' 10000
|
|
clear_input
|
|
ad keyboard dismiss || true
|
|
ad wait 'label="Message, @files, /commands"' 10000
|
|
|
|
snapshot_json "${ARTIFACTS_DIR}/baseline.json"
|
|
capture_screen "${ARTIFACTS_DIR}/baseline.png"
|
|
read -r input_x input_y baseline_input_height < <(
|
|
node "${ASSERT}" rect "${ARTIFACTS_DIR}/baseline.json" "Message, @files, /commands"
|
|
)
|
|
read -r model_x model_y _ < <(
|
|
node "${ASSERT}" rect "${ARTIFACTS_DIR}/baseline.json" "combined-model-selector"
|
|
)
|
|
read -r attach_x attach_y _ < <(
|
|
node "${ASSERT}" rect "${ARTIFACTS_DIR}/baseline.json" "message-input-attach-button"
|
|
)
|
|
read -r _ header_y _ < <(
|
|
node "${ASSERT}" rect "${ARTIFACTS_DIR}/baseline.json" "workspace-tab-switcher-trigger"
|
|
)
|
|
|
|
adb shell input keyevent HOME
|
|
adb shell am start -n "${APP_ID}/.MainActivity" >/dev/null
|
|
sleep 1
|
|
open_gboard "${input_x}" "${input_y}"
|
|
clear_focused_input_with_adb
|
|
keyboard_shift="$(read_keyboard_shift)"
|
|
adb shell input keyevent 76
|
|
sleep 1
|
|
capture_screen "${ARTIFACTS_DIR}/command-popover.png"
|
|
ad wait 'id="composer-autocomplete-popover"' 10000
|
|
ad wait 'text="/exit"' 10000
|
|
model_open_y="$((model_y - keyboard_shift))"
|
|
adb shell input tap "${model_x}" "${model_open_y}"
|
|
sleep 1
|
|
capture_screen "${ARTIFACTS_DIR}/model-selector-open.png"
|
|
adb shell ime set "${HELPER_IME}" >/dev/null
|
|
ad wait 'id="compact-provider-list"' 10000
|
|
ad press 'label="Close"' --settle
|
|
adb shell input tap "${input_x}" "${input_y}"
|
|
clear_focused_input_with_adb
|
|
ad wait 'editable=true' 10000
|
|
|
|
adb shell input keyevent HOME
|
|
adb shell am start -n "${APP_ID}/.MainActivity" >/dev/null
|
|
sleep 1
|
|
open_gboard "${input_x}" "${input_y}"
|
|
clear_focused_input_with_adb
|
|
adb shell input keyevent 76
|
|
sleep 1
|
|
adb shell ime set "${HELPER_IME}" >/dev/null
|
|
ad wait 'id="composer-autocomplete-popover"' 10000
|
|
ad wait 'text="/exit"' 10000
|
|
snapshot_json "${ARTIFACTS_DIR}/command-popover.json"
|
|
node "${ASSERT}" above-y \
|
|
"${ARTIFACTS_DIR}/command-popover.json" \
|
|
"/exit, Archive the current agent" \
|
|
"$((input_y - baseline_input_height / 2))"
|
|
adb shell input keyevent 67
|
|
ad wait 'label="Message, @files, /commands"' 10000
|
|
|
|
ad fill 'label="Message, @files, /commands"' "${MESSAGE}" --settle
|
|
snapshot_json "${ARTIFACTS_DIR}/multiline-first.json"
|
|
ad fill 'editable=true focused=true' "${MESSAGE}" --settle
|
|
snapshot_json "${ARTIFACTS_DIR}/multiline-second.json"
|
|
node "${ASSERT}" same-input-height \
|
|
"${ARTIFACTS_DIR}/multiline-first.json" \
|
|
"${ARTIFACTS_DIR}/multiline-second.json"
|
|
|
|
adb shell input keyevent HOME
|
|
adb shell am start -n "${APP_ID}/.MainActivity" >/dev/null
|
|
sleep 1
|
|
open_gboard "${input_x}" "${input_y}"
|
|
|
|
keyboard_shift="$(read_keyboard_shift)"
|
|
capture_screen "${ARTIFACTS_DIR}/keyboard-open.png"
|
|
|
|
node "${ASSERT}" same-header \
|
|
"${ARTIFACTS_DIR}/baseline.png" \
|
|
"${ARTIFACTS_DIR}/keyboard-open.png" \
|
|
"$((header_y + 48))"
|
|
|
|
adb shell ime set "${HELPER_IME}" >/dev/null
|
|
ad fill 'editable=true focused=true' "${LONG_MESSAGE}" --settle
|
|
open_gboard "${input_x}" "${input_y}"
|
|
capture_ui_xml "${ARTIFACTS_DIR}/long-draft-keyboard-open.xml"
|
|
capture_screen "${ARTIFACTS_DIR}/long-draft-keyboard-open.png"
|
|
node "${ASSERT}" xml-above-y \
|
|
"${ARTIFACTS_DIR}/long-draft-keyboard-open.xml" \
|
|
"Add attachment" \
|
|
"$(read_ime_top)"
|
|
display_density="$(adb shell wm density | awk '/density:/ { density = $NF } END { print density }')"
|
|
node "${ASSERT}" xml-composer-contained \
|
|
"${ARTIFACTS_DIR}/long-draft-keyboard-open.xml" \
|
|
"$(read_ime_top)" \
|
|
"${display_density}"
|
|
# Emptying a capped blank-line draft with Gboard's hold-to-delete must return the input to
|
|
# its baseline height. Fabric only re-measures the Android input when its text prop changes,
|
|
# so the editing input renders again after every edit
|
|
# (packages/app/src/components/ui/text-input/text-input.native.tsx). The sequence mirrors the
|
|
# report: grow to the cap, close and reopen the keyboard, hold delete from the middle of the
|
|
# draft, jump to the end, hold delete again. The first hold-delete must start mid-draft: from
|
|
# the end, the pre-fix input also returns to baseline. Backspace sits on Gboard's third key
|
|
# row on the paseo-api35 layout.
|
|
adb shell ime set "${HELPER_IME}" >/dev/null
|
|
ad fill 'editable=true' "${BLANK_LINE_DRAFT}" --settle
|
|
open_gboard "${input_x}" "${input_y}"
|
|
adb shell input keyevent BACK
|
|
wait_for_ime false
|
|
open_gboard "${input_x}" "${input_y}"
|
|
adb shell input keycombination 113 122
|
|
adb shell input keyevent 20 20 20 20 20 20 20 20
|
|
screen_width="$(adb shell wm size | sed -n 's/.*: \([0-9]*\)x.*/\1/p' | tail -1)"
|
|
backspace_x="$((screen_width - 85))"
|
|
backspace_y="$(($(read_ime_top) + 507))"
|
|
adb shell input swipe "${backspace_x}" "${backspace_y}" "${backspace_x}" "${backspace_y}" 8000
|
|
adb shell input keycombination 113 123
|
|
adb shell input swipe "${backspace_x}" "${backspace_y}" "${backspace_x}" "${backspace_y}" 12000
|
|
adb shell ime set "${HELPER_IME}" >/dev/null
|
|
ad wait 'label="Message, @files, /commands"' 10000
|
|
snapshot_json "${ARTIFACTS_DIR}/after-hold-delete.json"
|
|
node "${ASSERT}" same-input-height \
|
|
"${ARTIFACTS_DIR}/baseline.json" \
|
|
"${ARTIFACTS_DIR}/after-hold-delete.json"
|
|
|
|
adb shell ime set "${HELPER_IME}" >/dev/null
|
|
ad fill 'editable=true' "${MESSAGE}" --settle
|
|
open_gboard "${input_x}" "${input_y}"
|
|
keyboard_shift="$(read_keyboard_shift)"
|
|
|
|
read -r changes_x changes_y _ < <(
|
|
node "${ASSERT}" rect "${ARTIFACTS_DIR}/multiline-second.json" "Open Changes tab"
|
|
)
|
|
adb shell input tap "${changes_x}" "$((changes_y - keyboard_shift))"
|
|
ad wait 'id="changes-header"' 10000
|
|
capture_screen "${ARTIFACTS_DIR}/changes-control-open.png"
|
|
ad press 'label="Close Explorer sidebar"' --settle
|
|
ad wait "text=\"${AGENT_TITLE}\"" 10000
|
|
ad wait 'editable=true' 10000
|
|
|
|
open_gboard "${input_x}" "${input_y}"
|
|
keyboard_shift="$(read_keyboard_shift)"
|
|
adb shell input tap "${attach_x}" "$((attach_y - keyboard_shift))"
|
|
sleep 1
|
|
adb shell ime set "${HELPER_IME}" >/dev/null
|
|
ad wait 'id="message-input-attachment-menu-content"' 10000
|
|
ad press 540 600 --settle
|
|
|
|
open_gboard "${input_x}" "${input_y}"
|
|
keyboard_shift="$(read_keyboard_shift)"
|
|
adb shell input tap 983 "$((attach_y - keyboard_shift))"
|
|
wait_for_ime false
|
|
adb shell ime set "${HELPER_IME}" >/dev/null
|
|
ad wait text "keyboard invariant line one" 10000
|
|
snapshot_json "${ARTIFACTS_DIR}/after-submit.json"
|
|
node "${ASSERT}" has-exact-text "${ARTIFACTS_DIR}/after-submit.json" "${MESSAGE}"
|
|
node "${ASSERT}" same-input-height \
|
|
"${ARTIFACTS_DIR}/baseline.json" \
|
|
"${ARTIFACTS_DIR}/after-submit.json"
|
|
|
|
open_gboard "${input_x}" "${input_y}"
|
|
adb shell input keyevent BACK
|
|
wait_for_ime false
|
|
sleep 0.5
|
|
capture_screen "${ARTIFACTS_DIR}/hidden-focused-baseline.png"
|
|
|
|
open_gboard "${input_x}" "${input_y}"
|
|
node "${STALL_HERMES}" "${METRO_PORT}" 3000 "${APP_ID}" &
|
|
stall_pid="$!"
|
|
sleep 0.25
|
|
adb shell input keyevent BACK
|
|
wait_for_ime false
|
|
sleep 0.5
|
|
capture_screen "${ARTIFACTS_DIR}/hidden-during-js-stall.png"
|
|
wait "${stall_pid}"
|
|
capture_screen "${ARTIFACTS_DIR}/hidden-after-js-stall.png"
|
|
node "${ASSERT}" same-region \
|
|
"${ARTIFACTS_DIR}/hidden-focused-baseline.png" \
|
|
"${ARTIFACTS_DIR}/hidden-during-js-stall.png" \
|
|
1400 \
|
|
430
|
|
node "${ASSERT}" same-region \
|
|
"${ARTIFACTS_DIR}/hidden-focused-baseline.png" \
|
|
"${ARTIFACTS_DIR}/hidden-after-js-stall.png" \
|
|
1400 \
|
|
430
|
|
|
|
ad press 'id="menu-button"' --settle
|
|
ad press 'id="sidebar-global-new-workspace"' --settle
|
|
ad wait 'id="workspace-create-submit"' 10000
|
|
adb shell ime set "${HELPER_IME}" >/dev/null
|
|
ad fill 'editable=true' "x" --settle
|
|
snapshot_json "${ARTIFACTS_DIR}/new-workspace-short-draft.json"
|
|
read -r new_input_x new_input_y _ < <(
|
|
node "${ASSERT}" rect "${ARTIFACTS_DIR}/new-workspace-short-draft.json" "editable"
|
|
)
|
|
open_gboard "${new_input_x}" "${new_input_y}"
|
|
capture_ui_xml "${ARTIFACTS_DIR}/new-workspace-short-draft-keyboard-open.xml"
|
|
# Typing more lines moves the setup fields up with the composer instead of
|
|
# hiding their bottom rows behind it.
|
|
adb shell input keycombination 113 123
|
|
adb shell input keyevent 66 66 66
|
|
adb shell input text grown
|
|
sleep 1
|
|
capture_ui_xml "${ARTIFACTS_DIR}/new-workspace-grown-draft-keyboard-open.xml"
|
|
capture_screen "${ARTIFACTS_DIR}/new-workspace-grown-draft-keyboard-open.png"
|
|
node "${ASSERT}" xml-fields-follow-growth \
|
|
"${ARTIFACTS_DIR}/new-workspace-short-draft-keyboard-open.xml" \
|
|
"${ARTIFACTS_DIR}/new-workspace-grown-draft-keyboard-open.xml"
|
|
|
|
adb shell ime set "${HELPER_IME}" >/dev/null
|
|
ad fill 'editable=true' "${LONG_MESSAGE}" --settle
|
|
snapshot_json "${ARTIFACTS_DIR}/new-workspace-long-draft.json"
|
|
read -r new_input_x new_input_y new_input_height < <(
|
|
node "${ASSERT}" rect "${ARTIFACTS_DIR}/new-workspace-long-draft.json" "editable"
|
|
)
|
|
# The editor shrinks when the keyboard opens. Its bottom stays anchored, while
|
|
# its old center can move into the setup fields after the height constraint.
|
|
new_input_y="$((new_input_y + new_input_height / 2 - 8))"
|
|
open_gboard "${new_input_x}" "${new_input_y}"
|
|
capture_ui_xml "${ARTIFACTS_DIR}/new-workspace-long-draft-keyboard-open.xml"
|
|
capture_screen "${ARTIFACTS_DIR}/new-workspace-long-draft-keyboard-open.png"
|
|
node "${ASSERT}" xml-composer-contained \
|
|
"${ARTIFACTS_DIR}/new-workspace-long-draft-keyboard-open.xml" \
|
|
"$(read_ime_top)" \
|
|
"${display_density}"
|
|
|
|
# Dismissing the keyboard moves the composer down at its editing height and
|
|
# brings the setup fields back fully visible above it.
|
|
adb shell input keyevent BACK
|
|
wait_for_ime false
|
|
capture_ui_xml "${ARTIFACTS_DIR}/new-workspace-long-draft-keyboard-closed.xml"
|
|
capture_screen "${ARTIFACTS_DIR}/new-workspace-long-draft-keyboard-closed.png"
|
|
node "${ASSERT}" same-input-height \
|
|
"${ARTIFACTS_DIR}/new-workspace-long-draft-keyboard-open.xml" \
|
|
"${ARTIFACTS_DIR}/new-workspace-long-draft-keyboard-closed.xml"
|
|
node "${ASSERT}" xml-fields-above-composer \
|
|
"${ARTIFACTS_DIR}/new-workspace-long-draft-keyboard-closed.xml"
|
|
|
|
# All hosts run the same scenario; only navigation differs.
|
|
# For the shared scenario, tap the editor's blank right edge once. Tapping
|
|
# text again after opening invokes Android's spelling menu for long drafts.
|
|
open_editor_keyboard() {
|
|
local x="$1" y="$2"
|
|
local current_ime
|
|
current_ime="$(adb shell settings get secure default_input_method)"
|
|
if [[ "${current_ime}" != "${GBOARD_IME}" ]]; then
|
|
adb shell ime set "${GBOARD_IME}" >/dev/null
|
|
sleep 1
|
|
fi
|
|
if ! ime_is_visible; then adb shell input tap "${x}" "${y}"; fi
|
|
wait_for_ime true
|
|
}
|
|
|
|
prepare_composer() {
|
|
adb shell ime set "${HELPER_IME}" >/dev/null
|
|
wait_for_ime false
|
|
clear_input
|
|
wait_for_ime false
|
|
snapshot_json "${ARTIFACTS_DIR}/overlay-input.json"
|
|
read -r overlay_x overlay_y overlay_h < <(node "${ASSERT}" rect "${ARTIFACTS_DIR}/overlay-input.json" editable right)
|
|
if [[ "$1" == open ]]; then open_editor_keyboard "${overlay_x}" "${overlay_y}"; fi
|
|
}
|
|
press_composer_control() {
|
|
local x y h
|
|
snapshot_json "${ARTIFACTS_DIR}/overlay-input.json"
|
|
read -r x y h < <(node "${ASSERT}" rect "${ARTIFACTS_DIR}/overlay-input.json" "$1")
|
|
adb shell input tap "${x}" "${y}"
|
|
}
|
|
|
|
run_host_overlays() {
|
|
local host="$1"
|
|
for mode in closed open; do
|
|
for query in / @README; do
|
|
prepare_composer closed
|
|
adb shell input text "${query}"
|
|
if [[ "${mode}" == open ]]; then open_editor_keyboard "${overlay_x}" "${overlay_y}"; fi
|
|
ad wait 'id="composer-autocomplete-popover"' 10000
|
|
local kind=commands
|
|
if [[ "${query}" == @* ]]; then kind=files; fi
|
|
capture_screen "${ARTIFACTS_DIR}/${host}-${kind}-${mode}.png"
|
|
clear_focused_input_with_adb
|
|
echo "PASS host=${host}: ${kind} popover keyboard=${mode}"
|
|
done
|
|
prepare_composer "${mode}"
|
|
press_composer_control combined-model-selector
|
|
ad wait 'id="compact-provider-list"' 10000
|
|
capture_screen "${ARTIFACTS_DIR}/${host}-model-${mode}.png"
|
|
ad press 'label="Close"' --settle
|
|
echo "PASS host=${host}: model selector keyboard=${mode}"
|
|
prepare_composer "${mode}"
|
|
press_composer_control message-input-attach-button
|
|
ad wait 'id="message-input-attachment-menu-content"' 10000
|
|
capture_screen "${ARTIFACTS_DIR}/${host}-attachments-${mode}.png"
|
|
ad press 'id="message-input-attachment-menu-item-github"' --settle
|
|
ad wait 'text="Attach issue or PR"' 10000
|
|
capture_screen "${ARTIFACTS_DIR}/${host}-forge-${mode}.png"
|
|
adb shell input tap 540 400
|
|
ad wait 'id="message-input-attach-button"' 10000
|
|
echo "PASS host=${host}: attachment menu and forge picker keyboard=${mode}"
|
|
done
|
|
}
|
|
|
|
run_host_scroll() {
|
|
local host="$1"
|
|
local prefix="${ARTIFACTS_DIR}/${host}-native-scroll"
|
|
prepare_composer closed
|
|
if [[ "${host}" == new-workspace ]]; then
|
|
ad press 'id="workspace-create-isolation-trigger"' --settle
|
|
ad press 'text="Local"' --settle
|
|
fi
|
|
# Submit through each real host; draft/workspace creation then mounts its chat.
|
|
# A selectable mock model keeps this test independent of provider credentials.
|
|
press_composer_control combined-model-selector
|
|
ad wait 'id="model-search-all-input"' 10000
|
|
ad fill 'id="model-search-all-input"' 'Ten second stream'
|
|
ad wait 'id="model-row-mock-ten-second-stream"' 10000
|
|
ad press 'id="model-row-mock-ten-second-stream"' --settle
|
|
ad press 'label="Close"' --settle
|
|
adb shell ime set "${HELPER_IME}" >/dev/null
|
|
wait_for_ime false
|
|
local scroll_message
|
|
scroll_message="$(node -e 'process.stdout.write(Array.from({length: 50}, (_, i) => `Native scroll fixture ${i}: distinct history row after submitting from a dock host.`).join("\n"))')"
|
|
ad fill 'editable=true' "${scroll_message}"
|
|
if [[ "${host}" == new-workspace ]]; then
|
|
ad press 'id="workspace-create-submit"' --settle
|
|
else
|
|
ad press 'label="Send message"' --settle
|
|
fi
|
|
ad wait 'id="agent-chat-scroll"' 45000
|
|
sleep 15
|
|
ad wait 'text="(end of synthetic stream)"' 45000
|
|
if ime_is_visible; then adb shell input keyevent BACK; fi
|
|
wait_for_ime false
|
|
# Streaming/re-renders can mask responder interception. Exercise idle JS after
|
|
# submit, and again after returning to the bottom and letting the scrollbar fade.
|
|
sleep 5
|
|
capture_screen "${prefix}-submitted.png"
|
|
adb shell input swipe 540 800 540 1400 250
|
|
sleep 2
|
|
capture_screen "${prefix}-after-submit-swipe.png"
|
|
node "${ASSERT}" stream-moved "${prefix}-submitted.png" "${prefix}-after-submit-swipe.png"
|
|
for _ in 1 2 3 4 5 6; do adb shell input swipe 540 1400 540 600 120; done
|
|
sleep 5
|
|
capture_screen "${prefix}-bottom-idle.png"
|
|
adb shell input swipe 540 800 540 1400 250
|
|
sleep 2
|
|
capture_screen "${prefix}-after-idle-swipe.png"
|
|
node "${ASSERT}" stream-moved "${prefix}-bottom-idle.png" "${prefix}-after-idle-swipe.png"
|
|
echo "PASS host=${host}: native stream swipe after submit and after idle at bottom"
|
|
}
|
|
|
|
run_host_scenario() {
|
|
local host="$1"
|
|
local prefix="${ARTIFACTS_DIR}/${host}"
|
|
echo "BEGIN host=${host}"
|
|
adb shell ime set "${HELPER_IME}" >/dev/null
|
|
wait_for_ime false
|
|
clear_input
|
|
ad keyboard dismiss || true
|
|
snapshot_json "${prefix}-empty.json"
|
|
capture_screen "${prefix}-empty.png"
|
|
local x y height
|
|
read -r x y height < <(node "${ASSERT}" rect "${prefix}-empty.json" editable right)
|
|
open_editor_keyboard "${x}" "${y}"
|
|
capture_ui_xml "${prefix}-empty-open.xml"
|
|
capture_screen "${prefix}-empty-open.png"
|
|
node "${ASSERT}" same-input-height "${prefix}-empty.json" "${prefix}-empty-open.xml"
|
|
# The left gutter above a one-line composer is empty on every host.
|
|
local dismiss_x dismiss_y
|
|
read -r dismiss_x dismiss_y < <(node "${ASSERT}" xml-background-point "${prefix}-empty-open.xml" content)
|
|
adb shell input tap "${dismiss_x}" "${dismiss_y}"
|
|
wait_for_ime false
|
|
capture_ui_xml "${prefix}-content-dismissed.xml"
|
|
node "${ASSERT}" same-input-height "${prefix}-empty-open.xml" "${prefix}-content-dismissed.xml"
|
|
open_editor_keyboard "${x}" "${y}"
|
|
adb shell input text short
|
|
capture_ui_xml "${prefix}-short.xml"
|
|
adb shell input keyevent 66 66 66
|
|
adb shell input text grown
|
|
capture_ui_xml "${prefix}-grown.xml"
|
|
capture_screen "${prefix}-grown.png"
|
|
node "${ASSERT}" xml-content-follow-growth "${prefix}-short.xml" "${prefix}-grown.xml"
|
|
|
|
adb shell ime set "${HELPER_IME}" >/dev/null
|
|
wait_for_ime false
|
|
ad fill 'editable=true' "${LONG_MESSAGE}" --settle
|
|
snapshot_json "${prefix}-long.json"
|
|
read -r x y height < <(node "${ASSERT}" rect "${prefix}-long.json" editable right)
|
|
open_editor_keyboard "${x}" "${y}"
|
|
capture_ui_xml "${prefix}-long-open.xml"
|
|
capture_screen "${prefix}-long-open.png"
|
|
node "${ASSERT}" xml-composer-contained "${prefix}-long-open.xml" "$(read_ime_top)" "${display_density}"
|
|
# At the cap the header remains reachable even when all content is clipped.
|
|
read -r dismiss_x dismiss_y < <(node "${ASSERT}" xml-background-point "${prefix}-long-open.xml" header)
|
|
adb shell input tap "${dismiss_x}" "${dismiss_y}"
|
|
wait_for_ime false
|
|
capture_ui_xml "${prefix}-header-dismissed.xml"
|
|
node "${ASSERT}" same-input-height "${prefix}-long-open.xml" "${prefix}-header-dismissed.xml"
|
|
snapshot_json "${prefix}-header-dismissed.json"
|
|
read -r x y height < <(node "${ASSERT}" rect "${prefix}-header-dismissed.json" editable right)
|
|
open_editor_keyboard "${x}" "${y}"
|
|
adb shell input keyevent BACK
|
|
wait_for_ime false
|
|
capture_ui_xml "${prefix}-long-closed.xml"
|
|
capture_screen "${prefix}-long-closed.png"
|
|
node "${ASSERT}" same-input-height "${prefix}-long-open.xml" "${prefix}-long-closed.xml"
|
|
local navigation_top
|
|
navigation_top="$(adb shell dumpsys window | sed -n 's/.*type=navigationBars frame=\[0,\([0-9][0-9]*\)\].*/\1/p' | head -1)"
|
|
node "${ASSERT}" xml-composer-contained "${prefix}-long-closed.xml" "${navigation_top}" "${display_density}"
|
|
snapshot_json "${prefix}-long-closed.json"
|
|
read -r x y height < <(node "${ASSERT}" rect "${prefix}-long-closed.json" editable right)
|
|
open_editor_keyboard "${x}" "${y}"
|
|
capture_ui_xml "${prefix}-long-reopened.xml"
|
|
capture_screen "${prefix}-long-reopened.png"
|
|
node "${ASSERT}" same-input-height "${prefix}-long-open.xml" "${prefix}-long-reopened.xml"
|
|
|
|
adb shell ime set "${HELPER_IME}" >/dev/null
|
|
wait_for_ime false
|
|
ad fill 'editable=true' "${BLANK_LINE_DRAFT}" --settle
|
|
open_editor_keyboard "${x}" "${y}"
|
|
adb shell input keyevent BACK
|
|
wait_for_ime false
|
|
open_editor_keyboard "${x}" "${y}"
|
|
adb shell input keycombination 113 122
|
|
adb shell input keyevent 20 20 20 20 20 20 20 20
|
|
local delete_y="$(($(read_ime_top) + 507))"
|
|
adb shell input swipe "${backspace_x}" "${delete_y}" "${backspace_x}" "${delete_y}" 8000
|
|
adb shell input keycombination 113 123
|
|
adb shell input swipe "${backspace_x}" "${delete_y}" "${backspace_x}" "${delete_y}" 12000
|
|
capture_ui_xml "${prefix}-cleared.xml"
|
|
capture_screen "${prefix}-cleared.png"
|
|
node "${ASSERT}" same-input-height "${prefix}-empty-open.xml" "${prefix}-cleared.xml"
|
|
adb shell input keyevent BACK
|
|
wait_for_ime false
|
|
snapshot_json "${prefix}-cleared-closed.json"
|
|
read -r x y height < <(node "${ASSERT}" rect "${prefix}-cleared-closed.json" editable right)
|
|
capture_screen "${prefix}-stall-baseline.png"
|
|
capture_screen "${prefix}-cleared-closed.png"
|
|
open_editor_keyboard "${x}" "${y}"
|
|
node "${STALL_HERMES}" "${METRO_PORT}" 3000 "${APP_ID}" &
|
|
local stall_pid="$!"
|
|
sleep 0.25
|
|
adb shell input keyevent BACK
|
|
wait_for_ime false
|
|
sleep 0.5
|
|
capture_screen "${prefix}-during-stall.png"
|
|
wait "${stall_pid}"
|
|
capture_screen "${prefix}-after-stall.png"
|
|
node "${ASSERT}" same-region "${prefix}-stall-baseline.png" "${prefix}-during-stall.png" 1400 430
|
|
node "${ASSERT}" same-region "${prefix}-stall-baseline.png" "${prefix}-after-stall.png" 1400 430
|
|
echo "PASS host=${host}: growth, header/keyboard/safe-area bounds, close/reopen height, hold-delete baseline, content/header dismissal, JS-stall motion"
|
|
run_host_overlays "${host}"
|
|
run_host_scroll "${host}"
|
|
}
|
|
|
|
adb shell am start -a android.intent.action.VIEW -d "paseo://h/${SERVER_ID}/agent/${agent_id}" "${APP_ID}" >/dev/null
|
|
ad wait "text=\"${AGENT_TITLE}\"" 10000
|
|
run_host_scenario chat
|
|
ad keyboard dismiss || true
|
|
ad press 'id="workspace-header-menu-trigger"' --settle
|
|
ad wait 'id="workspace-header-new-agent"' 10000
|
|
ad press 'id="workspace-header-new-agent"' --settle
|
|
ad wait 'text="New Agent"' 10000
|
|
run_host_scenario workspace-draft
|
|
ad keyboard dismiss || true
|
|
ad press 'id="menu-button"' --settle
|
|
ad press 'id="sidebar-global-new-workspace"' --settle
|
|
ad wait 'id="workspace-create-submit"' 10000
|
|
run_host_scenario new-workspace
|
|
|
|
echo "Composer keyboard invariants passed"
|