2013年11月26日火曜日

Espresso で EditTextPreference に文字を入力する方法

Espresso で EditText に日本語を入力する方法」と「Espresso で Preference をクリックさせる」の応用です。

EditTextPreference はダイアログ内の EditText をコードから生成し、Id として com.android.internal.R.id.edit をセットしています。

http://tools.oesf.biz/android-4.4.0_r1.0/xref/frameworks/base/core/java/android/preference/EditTextPreference.java#53 45 public class EditTextPreference extends DialogPreference { 46 /** 47 * The edit text shown in the dialog. 48 */ 49 private EditText mEditText; 50 51 private String mText; 52 53 public EditTextPreference(Context context, AttributeSet attrs, int defStyle) { 54 super(context, attrs, defStyle); 55 56 mEditText = new EditText(context, attrs); 57 58 // Give it an ID so it can be saved/restored 59 mEditText.setId(com.android.internal.R.id.edit); 60 61 /* 62 * The preference framework and view framework both have an 'enabled' 63 * attribute. Most likely, the 'enabled' specified in this XML is for 64 * the preference framework, but it was also given to the view framework. 65 * We reset the enabled state. 66 */ 67 mEditText.setEnabled(true); 68 } internal なので Id を指定する方法は使えません。そこで、isAssignableFrom()を使います。(withClassName()でも実現できます) public void testEditTextPreference() { // EditTextPreference をクリック onData(withPreferenceKey("edit_text_pref1")).perform(click()); onView(isAssignableFrom(EditText.class)).perform(clearText(), new InputTextAction("エスプレッソよりカフェラテ派")); onView(withText("OK")).perform(click()); // 例えば入力値が summary に表示されるような実装になっているなら、それをチェックできる onData(withPreferenceKey("edit_text_pref1")) .onChildView(withId(android.R.id.summary)) .check(matches(withText("エスプレッソよりカフェラテ派"))); }


Espresso で EditText に日本語を入力する方法

Espresso には、テキストをタイプする ViewActions.typeText() というメソッドが用意されています。
このメソッドは、引数で渡されたテキストの各文字に対応する KeyCode を入力するものです。
そのため、typeText("日本語") としても"日本語"は入力されませんし、ソフトキーボードが日本語キーボードのときに typeText("andoroido") とすると、"あんどろいど" と入力されます。
また困ったことに、ソフトキーボードが日本語キーボードのときに typeText("12345") とすると、全角で入力されます。orz


日本語を入力するには、setText() で直接セットするしかありません。
そのための ViewAction を実装したクラスを用意しました。

public void testInputJapanese() { onView(R.id.editText1).perform(clearText(), new InputTextAction("日本語")); onView(R.id.editText1).check(matches(withText("日本語"))); } public final class InputTextAction implements ViewAction { private final String mText; public InputTextAction(String text) { checkNotNull(text); mText = text; } @SuppressWarnings("unchecked") @Override public Matcher getConstraints() { return allOf(isDisplayed(), isAssignableFrom(EditText.class)); } @Override public void perform(UiController uiController, View view) { ((EditText) view).setText(mText); } @Override public String getDescription() { return "set text"; } }


ちなみに typeText() の実体は TypeTextAction クラスです。

ViewActions.java public final class ViewActions { ... public static ViewAction typeText(String stringToBeTyped) { return new TypeTextAction(stringToBeTyped); } } こちらでは UiController の injectString() を利用しています。また、SearchView にも入力できるみたいです。

TypeTextAction.java public final class TypeTextAction implements ViewAction { private static final String TAG = TypeTextAction.class.getSimpleName(); private final String stringToBeTyped; /** * Constructs {@link TypeTextAction} with given string. If the string is empty it results in no-op * (nothing is typed). * * @param stringToBeTyped String To be typed by {@link TypeTextAction} */ public TypeTextAction(String stringToBeTyped) { checkNotNull(stringToBeTyped); this.stringToBeTyped = stringToBeTyped; } @SuppressWarnings("unchecked") @Override public Matcher getConstraints() { Matcher matchers = allOf(isDisplayed()); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { return allOf(matchers, supportsInputMethods()); } else { // SearchView does not support input methods itself (rather it delegates to an internal text // view for input). return allOf(matchers, anyOf(supportsInputMethods(), isAssignableFrom(SearchView.class))); } } @Override public void perform(UiController uiController, View view) { // No-op if string is empty. if (stringToBeTyped.length() == 0) { Log.w(TAG, "Supplied string is empty resulting in no-op (nothing is typed)."); return; } // Perform a click. new GeneralClickAction(Tap.SINGLE, GeneralLocation.CENTER, Press.PINPOINT) .perform(uiController, view); uiController.loopMainThreadUntilIdle(); try { if (!uiController.injectString(stringToBeTyped)) { Log.e(TAG, "Failed to type text: " + stringToBeTyped); throw new PerformException.Builder() .withActionDescription(this.getDescription()) .withViewDescription(HumanReadables.describe(view)) .withCause(new RuntimeException("Failed to type text: " + stringToBeTyped)) .build(); } } catch (InjectEventSecurityException e) { Log.e(TAG, "Failed to type text: " + stringToBeTyped); throw new PerformException.Builder() .withActionDescription(this.getDescription()) .withViewDescription(HumanReadables.describe(view)) .withCause(e) .build(); } } @Override public String getDescription() { return "type text"; } } UiController はインタフェースで、実装クラスは UiControllerImpl.java です。

UiControllerImpl.java @Override public boolean injectString(String str) throws InjectEventSecurityException { checkNotNull(str); checkState(Looper.myLooper() == mainLooper, "Expecting to be on main thread!"); initialize(); // No-op if string is empty. if (str.length() == 0) { Log.w(TAG, "Supplied string is empty resulting in no-op (nothing is typed)."); return true; } boolean eventInjected = false; KeyCharacterMap keyCharacterMap = getKeyCharacterMap(); // TODO(user): Investigate why not use (as suggested in javadoc of keyCharacterMap.getEvents): // http://developer.android.com/reference/android/view/KeyEvent.html#KeyEvent(long, // java.lang.String, int, int) KeyEvent[] events = keyCharacterMap.getEvents(str.toCharArray()); checkNotNull(events, "Failed to get events for string " + str); Log.d(TAG, String.format("Injecting string: \"%s\"", str)); for (KeyEvent event : events) { checkNotNull(event, String.format("Failed to get event for character (%c) with key code (%s)", event.getKeyCode(), event.getUnicodeChar())); eventInjected = false; for (int attempts = 0; !eventInjected && attempts < 4; attempts++) { attempts++; // We have to change the time of an event before injecting it because // all KeyEvents returned by KeyCharacterMap.getEvents() have the same // time stamp and the system rejects too old events. Hence, it is // possible for an event to become stale before it is injected if it // takes too long to inject the preceding ones. event = KeyEvent.changeTimeRepeat(event, SystemClock.uptimeMillis(), 0); eventInjected = injectKeyEvent(event); } if (!eventInjected) { Log.e(TAG, String.format("Failed to inject event for character (%c) with key code (%s)", event.getUnicodeChar(), event.getKeyCode())); break; } } return eventInjected; }


Espresso で Preference をクリックさせる

Matcher 書かないとダメっぽかったです。
PreferenceMatcher が用意されていたので利用しました。
import static com.google.common.base.Preconditions.checkNotNull; ... public class EspressoTest extends ActivityInstrumentationTestCase2<MainPreferenceActivity> { public EspressoTest() { super(MainPreferenceActivity.class); } @Override public void setUp() throws Exception { super.setUp(); // Espresso will not launch our activity for us, we must launch it via // getActivity(). getActivity(); } // Preference のキーを指定して、対応するビューをクリックする public void testPreference() { onData(withPreferenceKey("pref-key")).perform(click()); } public static Matcher<Object> withPreferenceKey(final Matcher<Preference> preferenceMatcher) { checkNotNull(preferenceMatcher); return new BoundedMatcher<Object, Preference>(Preference.class) { @Override public void describeTo(Description description) { description.appendText("with preference key: "); preferenceMatcher.describeTo(description); } @Override protected boolean matchesSafely(Preference pref) { return preferenceMatcher.matches(pref); } }; } public static Matcher<Object> withPreferenceKey(String expectedText) { checkNotNull(expectedText); return withPreferenceKey(PreferenceMatchers.withKey(expectedText)); } }

応用で、Preference をクリック → なんかする → summary が適切な値になっていることをチェック public void testPreference() { // Preference をクリック onData(withPreferenceKey("pref-key")).perform(click()); // クリック先でごにょごにょ // summary が適切な値になっていることをチェック // summary の Id が android.R.id.summary であることを利用 onData(withPreferenceKey("pref-key")) .onChildView(withId(android.R.id.summary)) .check(matches(withText("correct summary value"))); }