Skip to content

Commit 7fedc71

Browse files
committed
fix(e2e): clear wrapped fields from any caret and type non-ASCII via clipboard
Step 33 has never passed in CI. Two independent harness bugs, both readable straight off the failure log; the product path is correct. Clearing assumed KEYCODE_MOVE_END reaches the end of the buffer, but it only reaches the end of the current soft line. The topic draft is a multiline field and the seeded topic wraps, so a centre tap plus MOVE_END plus backspaces deleted the head and left the tail: the probe write landed as "<probe> deterministic history", exactly the residue plus a fresh insert at position 0. `adb shell input text` synthesizes key events through the device KeyCharacterMap, which has no sequence for U+2014, and one such character aborts the whole command so nothing is typed. The restoration write left the header at just " deterministic history" -- the partial-clear residue with zero characters entered. Had only the dash been dropped, the ASCII remainder would have shown. - add _e2e_clear_field, deleting both backward and forward from wherever the caret landed, and use it for the generic field helper and the topic draft - route any text with non-printable-ASCII bytes through the device clipboard and KEYCODE_PASTE, which Compose maps to paste-at-caret - add E2eClipboardReceiver, e2e build type only, to set the clipboard from the harness; exported so the adb shell uid can reach it - fail the topic step when typing fails instead of saving a half-cleared draft Fixing lib.sh rather than making the fixture topic ASCII: the restore contract covers maintainer-configured channels whose topics can hold arbitrary unicode, and any later journey typing user-visible strings meets the same limitation.
1 parent 2836264 commit 7fedc71

4 files changed

Lines changed: 104 additions & 11 deletions

File tree

app/src/e2e/AndroidManifest.xml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools">
34

45
<application>
56
<!-- Test-only host for component instrumentation; see ComposeHostActivity. -->
@@ -8,6 +9,18 @@
89
android:exported="false"
910
android:theme="@style/Theme.Motd"
1011
android:windowSoftInputMode="adjustResize" />
12+
13+
<!-- E2E-only unicode text-entry bridge for test/e2e/lib.sh; see E2eClipboardReceiver.
14+
Exported on purpose: the adb shell uid must reach it, this build type is never
15+
shipped, and the worst a caller can do is set the emulator clipboard. -->
16+
<receiver
17+
android:name=".E2eClipboardReceiver"
18+
android:exported="true"
19+
tools:ignore="ExportedReceiver">
20+
<intent-filter>
21+
<action android:name="io.github.trevarj.motd.e2e.SET_CLIPBOARD" />
22+
</intent-filter>
23+
</receiver>
1124
</application>
1225

1326
</manifest>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package io.github.trevarj.motd
2+
3+
import android.content.BroadcastReceiver
4+
import android.content.ClipData
5+
import android.content.ClipboardManager
6+
import android.content.Context
7+
import android.content.Intent
8+
import android.util.Base64
9+
10+
/**
11+
* E2E-only bridge for non-ASCII text entry from the shell harness.
12+
*
13+
* `adb shell input text` synthesizes key events through the device [android.view.KeyCharacterMap],
14+
* which has no key sequence for characters outside the virtual keyboard's map (for example the
15+
* U+2014 em dash in the seeded channel topic); a single such character makes the whole command
16+
* abort and type nothing. test/e2e/lib.sh therefore broadcasts arbitrary text here base64-encoded,
17+
* then presses KEYCODE_PASTE, which Compose maps to paste-at-caret in the focused text field.
18+
*
19+
* Ships only in the e2e build type (never debug/release) and is exported solely so the adb shell
20+
* uid can reach it while the harness drives the foreground app.
21+
*/
22+
class E2eClipboardReceiver : BroadcastReceiver() {
23+
override fun onReceive(context: Context, intent: Intent) {
24+
val encoded = intent.getStringExtra(EXTRA_TEXT_B64) ?: return
25+
// Base64 keeps the payload intact across the host shell, adb's re-quoting, and the device
26+
// shell; decoding here is the only place the raw text is materialized.
27+
val text = String(Base64.decode(encoded, Base64.DEFAULT), Charsets.UTF_8)
28+
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
29+
clipboard.setPrimaryClip(ClipData.newPlainText("motd-e2e", text))
30+
}
31+
32+
companion object {
33+
const val EXTRA_TEXT_B64 = "text_b64"
34+
}
35+
}

test/e2e/lib.sh

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -386,13 +386,42 @@ long_press_tag_prefix_containing_text() {
386386

387387
# --- text input ------------------------------------------------------------
388388

389+
# _e2e_clear_field <count> — empty the focused text field by deleting up to
390+
# <count> characters on BOTH sides of the cursor. Never trust cursor
391+
# positioning: a center tap drops the caret mid-text, and KEYCODE_MOVE_END only
392+
# reaches the end of the current SOFT line, so on a wrapped multiline field
393+
# "MOVE_END + backspaces" deletes the head and leaves the tail (nightly step 33
394+
# saved "<probe> deterministic history" exactly this way). Backward then forward
395+
# deletes clear the field from any caret position.
396+
_e2e_clear_field() {
397+
local n="$1" i
398+
local dels=() fwds=()
399+
for ((i = 0; i < n; i++)); do dels+=(67); fwds+=(112); done # 67=DEL, 112=FORWARD_DEL
400+
adb_shell input keyevent "${dels[@]}"
401+
adb_shell input keyevent "${fwds[@]}"
402+
}
403+
389404
# _e2e_send_text <text> — type text via adb `input text`, safely. adb `input
390405
# text` treats space as an argument separator and interprets a few characters
391406
# specially; we escape spaces as %s and backslash-escape the shell-significant
392407
# ASCII punctuation so arbitrary values (passwords, URLs) type verbatim.
408+
# Text containing anything outside printable ASCII cannot go through `input
409+
# text` at all and is routed through the clipboard instead.
393410
_e2e_send_text() {
394411
local text="$1" out=""
395412
local i ch
413+
# Empty text is a valid no-op (Phase D may restore an empty original topic; the field was
414+
# already cleared). A bare `input text` with no argument would error instead.
415+
[ -n "$text" ] || return 0
416+
# `input text` synthesizes key events via the device KeyCharacterMap, which
417+
# has no key sequence for characters like U+2014; one such character makes
418+
# the whole command abort and type NOTHING (the nightly's topic restoration
419+
# failed exactly this way). Detect any non-printable-ASCII byte and use the
420+
# clipboard path, which handles arbitrary unicode.
421+
if LC_ALL=C printf '%s' "$text" | grep -q '[^ -~]'; then
422+
_e2e_send_text_via_clipboard "$text"
423+
return
424+
fi
396425
for (( i=0; i<${#text}; i++ )); do
397426
ch="${text:$i:1}"
398427
case "$ch" in
@@ -407,6 +436,25 @@ _e2e_send_text() {
407436
adb_shell input text "$out"
408437
}
409438

439+
# _e2e_send_text_via_clipboard <text> — non-ASCII entry path. Hands the text
440+
# (base64, so it survives both shells verbatim) to the e2e build's
441+
# E2eClipboardReceiver, which puts it on the device clipboard, then presses
442+
# KEYCODE_PASTE; Compose's key mapping pastes it at the caret of the focused
443+
# field with no modifier key needed. Requires the :app e2e variant (the only
444+
# variant the emulator harness can install — debug/release are arm64-only).
445+
_e2e_send_text_via_clipboard() {
446+
local b64
447+
b64="$(printf '%s' "$1" | base64 | tr -d '\n')"
448+
adb_shell am broadcast \
449+
-n "${MOTD_PKG}/io.github.trevarj.motd.E2eClipboardReceiver" \
450+
-a io.github.trevarj.motd.e2e.SET_CLIPBOARD \
451+
--es text_b64 "$b64" >/dev/null 2>&1 || {
452+
fail "clipboard broadcast failed; is the installed APK the e2e variant?"
453+
return 1
454+
}
455+
adb_shell input keyevent 279 # KEYCODE_PASTE
456+
}
457+
410458
# _e2e_input_by_fn <bounds-fn> <selector> <text> <label> — tap a field to focus
411459
# it, type the text, then close the keyboard (BACK). Caller should re-dump after
412460
# because the IME open/close shifts bounds (runbook flags every field step).
@@ -426,10 +474,8 @@ _e2e_input_by_fn() {
426474
# Give the IME a beat to attach focus before typing.
427475
sleep 1
428476
# Clear any pre-filled/default content (e.g. the Port field defaults to 6697) so typing
429-
# REPLACES rather than appends. Move caret to end, then delete a generous run backward.
430-
# 123=MOVE_END, 67=DEL; these fields are short (host/port/nick/user/pass).
431-
adb_shell input keyevent 123
432-
adb_shell input keyevent 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67
477+
# REPLACES rather than appends. These fields are short (host/port/nick/user/pass).
478+
_e2e_clear_field 40
433479
_e2e_send_text "$text"
434480
# Close the soft keyboard so subsequent dumps see the settled layout.
435481
adb_shell input keyevent 4 # KEYCODE_BACK

test/e2e/runbook.sh

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ MOTD_ROOT_READY_LABEL="Connected as ${MOTD_SOJU_USER}"
109109
# lib.sh. Keep this journey-specific helper on the stable field tag, and accept an empty original
110110
# topic so Phase D can restore either seeded or manually configured channels.
111111
input_topic_draft() {
112-
local text="$1" bounds xy i
112+
local text="$1" bounds xy
113113
dump || { fail "dump failed locating topic draft"; return 1; }
114114
bounds="$(bounds_of_tag channelinfo_topic_edit_text)"
115115
if [ -z "$bounds" ]; then
@@ -120,11 +120,10 @@ input_topic_draft() {
120120
# shellcheck disable=SC2086 # _e2e_center intentionally returns x y
121121
adb_shell input tap $xy
122122
sleep 1
123-
adb_shell input keyevent 123
124-
local deletes=()
125-
for ((i = 0; i < 160; i++)); do deletes+=(67); done
126-
adb_shell input keyevent "${deletes[@]}"
127-
_e2e_send_text "$text"
123+
# The draft wraps across soft lines, so clearing must not assume the caret can be moved to the
124+
# true end of the buffer; _e2e_clear_field deletes both directions from wherever the tap put it.
125+
_e2e_clear_field 160
126+
_e2e_send_text "$text" || { fail "could not type topic draft"; return 1; }
128127
adb_shell input keyevent 4
129128
ok "input topic draft <= \"${text}\""
130129
}

0 commit comments

Comments
 (0)