@@ -4,6 +4,7 @@ import { promises as fs } from 'node:fs';
44import os from 'node:os' ;
55import path from 'node:path' ;
66import {
7+ fillAndroid ,
78 inferAndroidAppName ,
89 isAmStartError ,
910 listAndroidApps ,
@@ -737,6 +738,115 @@ test('typeAndroid uses adb input text for ascii text', async () => {
737738 ) ;
738739} ) ;
739740
741+ test ( 'typeAndroid passes shell-sensitive ascii text to adb input text' , async ( ) => {
742+ await withMockedAdb (
743+ 'agent-device-android-type-ascii-special-' ,
744+ '#!/bin/sh\nprintf "%s\\n" "$@" > "$AGENT_DEVICE_TEST_ARGS_FILE"\nexit 0\n' ,
745+ async ( { argsLogPath, device } ) => {
746+ await typeAndroid ( device , 'curtis.layne+test+73kmc@uber.com' ) ;
747+ const args = ( await fs . readFile ( argsLogPath , 'utf8' ) )
748+ . trim ( )
749+ . split ( '\n' )
750+ . filter ( Boolean ) ;
751+ assert . deepEqual ( args , [
752+ '-s' ,
753+ 'emulator-5554' ,
754+ 'shell' ,
755+ 'input' ,
756+ 'text' ,
757+ 'curtis.layne+test+73kmc@uber.com' ,
758+ ] ) ;
759+ } ,
760+ ) ;
761+ } ) ;
762+
763+ test ( 'typeAndroid preserves percent signs while encoding spaces' , async ( ) => {
764+ await withMockedAdb (
765+ 'agent-device-android-type-ascii-percent-' ,
766+ '#!/bin/sh\nprintf "%s\\n" "$@" > "$AGENT_DEVICE_TEST_ARGS_FILE"\nexit 0\n' ,
767+ async ( { argsLogPath, device } ) => {
768+ await typeAndroid ( device , '50% complete' ) ;
769+ const args = ( await fs . readFile ( argsLogPath , 'utf8' ) )
770+ . trim ( )
771+ . split ( '\n' )
772+ . filter ( Boolean ) ;
773+ assert . deepEqual ( args , [
774+ '-s' ,
775+ 'emulator-5554' ,
776+ 'shell' ,
777+ 'input' ,
778+ 'text' ,
779+ '50%%scomplete' ,
780+ ] ) ;
781+ } ,
782+ ) ;
783+ } ) ;
784+
785+ test ( 'fillAndroid falls back to clipboard paste when adb input text truncates' , async ( ) => {
786+ await withMockedAdb (
787+ 'agent-device-android-fill-fallback-' ,
788+ [
789+ '#!/bin/sh' ,
790+ 'STATE_FILE="$(dirname "$AGENT_DEVICE_TEST_ARGS_FILE")/fill_state.txt"' ,
791+ 'CLIP_FILE="$(dirname "$AGENT_DEVICE_TEST_ARGS_FILE")/clipboard_state.txt"' ,
792+ 'printf "__CMD__\\n" >> "$AGENT_DEVICE_TEST_ARGS_FILE"' ,
793+ 'printf "%s\\n" "$@" >> "$AGENT_DEVICE_TEST_ARGS_FILE"' ,
794+ 'if [ "$1" = "-s" ]; then' ,
795+ ' shift' ,
796+ ' shift' ,
797+ 'fi' ,
798+ 'if [ "$1" = "shell" ] && [ "$2" = "input" ] && [ "$3" = "tap" ]; then' ,
799+ ' exit 0' ,
800+ 'fi' ,
801+ 'if [ "$1" = "shell" ] && [ "$2" = "input" ] && [ "$3" = "keyevent" ] && [ "$4" = "KEYCODE_MOVE_END" ]; then' ,
802+ ' exit 0' ,
803+ 'fi' ,
804+ 'if [ "$1" = "shell" ] && [ "$2" = "input" ] && [ "$3" = "keyevent" ] && [ "$4" = "KEYCODE_DEL" ]; then' ,
805+ ' : > "$STATE_FILE"' ,
806+ ' exit 0' ,
807+ 'fi' ,
808+ 'if [ "$1" = "shell" ] && [ "$2" = "input" ] && [ "$3" = "text" ]; then' ,
809+ ' # Simulate WebView truncation on shell text input with special chars.' ,
810+ ' if [ "$4" = "curtis.layne+test+73kmc@uber.com" ]; then' ,
811+ ' printf "curti" > "$STATE_FILE"' ,
812+ ' else' ,
813+ ' printf "%s" "$4" > "$STATE_FILE"' ,
814+ ' fi' ,
815+ ' exit 0' ,
816+ 'fi' ,
817+ 'if [ "$1" = "shell" ] && [ "$2" = "cmd" ] && [ "$3" = "clipboard" ] && [ "$4" = "set" ] && [ "$5" = "text" ]; then' ,
818+ ' printf "%s" "$6" > "$CLIP_FILE"' ,
819+ ' exit 0' ,
820+ 'fi' ,
821+ 'if [ "$1" = "shell" ] && [ "$2" = "input" ] && [ "$3" = "keyevent" ] && [ "$4" = "KEYCODE_PASTE" ]; then' ,
822+ ' cat "$CLIP_FILE" > "$STATE_FILE"' ,
823+ ' exit 0' ,
824+ 'fi' ,
825+ 'if [ "$1" = "shell" ] && [ "$2" = "input" ] && [ "$3" = "keyevent" ] && [ "$4" = "279" ]; then' ,
826+ ' cat "$CLIP_FILE" > "$STATE_FILE"' ,
827+ ' exit 0' ,
828+ 'fi' ,
829+ 'if [ "$1" = "exec-out" ] && [ "$2" = "uiautomator" ] && [ "$3" = "dump" ] && [ "$4" = "/dev/tty" ]; then' ,
830+ ' text="$(cat "$STATE_FILE" 2>/dev/null)"' ,
831+ ' printf "<?xml version=\\"1.0\\" encoding=\\"UTF-8\\"?><hierarchy><node class=\\"android.widget.EditText\\" text=\\"%s\\" focused=\\"true\\" bounds=\\"[0,0][200,100]\\"/></hierarchy>" "$text"' ,
832+ ' exit 0' ,
833+ 'fi' ,
834+ 'echo "unexpected args: $@" >&2' ,
835+ 'exit 1' ,
836+ '' ,
837+ ] . join ( '\n' ) ,
838+ async ( { argsLogPath, device } ) => {
839+ await fillAndroid ( device , 10 , 10 , 'curtis.layne+test+73kmc@uber.com' ) ;
840+ const logged = await fs . readFile ( argsLogPath , 'utf8' ) ;
841+ assert . match ( logged , / s h e l l \n i n p u t \n t e x t \n c u r t i s \. l a y n e \+ t e s t \+ 7 3 k m c @ u b e r \. c o m / ) ;
842+ assert . match ( logged , / s h e l l \n c m d \n c l i p b o a r d \n s e t \n t e x t \n c u r t i s \. l a y n e \+ t e s t \+ 7 3 k m c @ u b e r \. c o m / ) ;
843+ assert . match ( logged , / s h e l l \n i n p u t \n k e y e v e n t \n K E Y C O D E _ P A S T E / ) ;
844+ const shellInputTextCount = ( logged . match ( / s h e l l \n i n p u t \n t e x t \n / g) ?? [ ] ) . length ;
845+ assert . equal ( shellInputTextCount , 1 ) ;
846+ } ,
847+ ) ;
848+ } ) ;
849+
740850test ( 'typeAndroid reports clear error when unicode input is unsupported' , async ( ) => {
741851 await withMockedAdb (
742852 'agent-device-android-type-unicode-unsupported-' ,
0 commit comments