| Index: content/public/android/junit/src/org/chromium/content/browser/input/ThreadedInputConnectionFactoryTest.java
 | 
| diff --git a/content/public/android/junit/src/org/chromium/content/browser/input/ThreadedInputConnectionFactoryTest.java b/content/public/android/junit/src/org/chromium/content/browser/input/ThreadedInputConnectionFactoryTest.java
 | 
| new file mode 100644
 | 
| index 0000000000000000000000000000000000000000..af40a611c46b2a5037d815fdfc96af624186158e
 | 
| --- /dev/null
 | 
| +++ b/content/public/android/junit/src/org/chromium/content/browser/input/ThreadedInputConnectionFactoryTest.java
 | 
| @@ -0,0 +1,290 @@
 | 
| +// Copyright 2016 The Chromium Authors. All rights reserved.
 | 
| +// Use of this source code is governed by a BSD-style license that can be
 | 
| +// found in the LICENSE file.
 | 
| +
 | 
| +package org.chromium.content.browser.input;
 | 
| +
 | 
| +import static org.junit.Assert.assertFalse;
 | 
| +import static org.junit.Assert.assertNotNull;
 | 
| +import static org.junit.Assert.assertNull;
 | 
| +import static org.junit.Assert.assertTrue;
 | 
| +import static org.junit.Assert.fail;
 | 
| +import static org.mockito.ArgumentMatchers.any;
 | 
| +import static org.mockito.Mockito.doAnswer;
 | 
| +import static org.mockito.Mockito.inOrder;
 | 
| +import static org.mockito.Mockito.when;
 | 
| +
 | 
| +import android.content.Context;
 | 
| +import android.os.Handler;
 | 
| +import android.view.View;
 | 
| +import android.view.inputmethod.EditorInfo;
 | 
| +import android.view.inputmethod.InputConnection;
 | 
| +import android.view.inputmethod.InputMethodManager;
 | 
| +
 | 
| +import org.chromium.base.ThreadUtils;
 | 
| +import org.chromium.base.test.util.Feature;
 | 
| +import org.chromium.testing.local.LocalRobolectricTestRunner;
 | 
| +import org.junit.Before;
 | 
| +import org.junit.Test;
 | 
| +import org.junit.runner.RunWith;
 | 
| +import org.mockito.InOrder;
 | 
| +import org.mockito.Mock;
 | 
| +import org.mockito.Mockito;
 | 
| +import org.mockito.MockitoAnnotations;
 | 
| +import org.mockito.invocation.InvocationOnMock;
 | 
| +import org.mockito.stubbing.Answer;
 | 
| +import org.robolectric.Robolectric;
 | 
| +import org.robolectric.annotation.Config;
 | 
| +import org.robolectric.internal.ShadowExtractor;
 | 
| +import org.robolectric.shadows.ShadowLooper;
 | 
| +
 | 
| +import java.util.concurrent.Callable;
 | 
| +
 | 
| +/**
 | 
| + * Unit tests for {@ThreadedInputConnectionFactory}.
 | 
| + */
 | 
| +@RunWith(LocalRobolectricTestRunner.class)
 | 
| +@Config(manifest = Config.NONE)
 | 
| +public class ThreadedInputConnectionFactoryTest {
 | 
| +
 | 
| +    /**
 | 
| +     * A testable version of ThreadedInputConnectionFactory.
 | 
| +     */
 | 
| +    private class TestFactory extends ThreadedInputConnectionFactory {
 | 
| +
 | 
| +        private boolean mSucceeded;
 | 
| +        private boolean mFailed;
 | 
| +
 | 
| +        TestFactory(InputMethodManagerWrapper inputMethodManagerWrapper) {
 | 
| +            super(inputMethodManagerWrapper);
 | 
| +        }
 | 
| +
 | 
| +        @Override
 | 
| +        protected Handler createHandler() {
 | 
| +            mImeHandler = super.createHandler();
 | 
| +            mImeShadowLooper = (ShadowLooper) ShadowExtractor.extract(mImeHandler.getLooper());
 | 
| +            return mImeHandler;
 | 
| +        }
 | 
| +
 | 
| +        @Override
 | 
| +        protected ThreadedInputConnectionProxyView createProxyView(Handler handler,
 | 
| +                View containerView) {
 | 
| +            return mProxyView;
 | 
| +        }
 | 
| +
 | 
| +        @Override
 | 
| +        protected InputMethodUma createInputMethodUma() {
 | 
| +            return null;
 | 
| +        }
 | 
| +
 | 
| +        @Override
 | 
| +        protected void onRegisterProxyViewSuccess() {
 | 
| +            mSucceeded = true;
 | 
| +        }
 | 
| +
 | 
| +        @Override
 | 
| +        protected void onRegisterProxyViewFailure() {
 | 
| +            mFailed = true;
 | 
| +        }
 | 
| +
 | 
| +        public boolean hasFailed() {
 | 
| +            return mFailed;
 | 
| +        }
 | 
| +        public boolean hasSucceeded() {
 | 
| +            return mSucceeded;
 | 
| +        }
 | 
| +    }
 | 
| +
 | 
| +    @Mock
 | 
| +    private ImeAdapter mImeAdapter;
 | 
| +    @Mock
 | 
| +    private View mContainerView;
 | 
| +    @Mock
 | 
| +    private ThreadedInputConnectionProxyView mProxyView;
 | 
| +    @Mock
 | 
| +    private InputMethodManager mInputMethodManager;
 | 
| +    @Mock
 | 
| +    private Context mContext;
 | 
| +
 | 
| +    private EditorInfo mEditorInfo;
 | 
| +    private Handler mImeHandler;
 | 
| +    private Handler mUiHandler;
 | 
| +    private ShadowLooper mImeShadowLooper;
 | 
| +    private TestFactory mFactory;
 | 
| +    private InputConnection mInputConnection;
 | 
| +    private InOrder mInOrder;
 | 
| +    private boolean mWindowFocusChanged;
 | 
| +
 | 
| +    @Before
 | 
| +    public void setUp() throws Exception {
 | 
| +        MockitoAnnotations.initMocks(this);
 | 
| +
 | 
| +        mEditorInfo = new EditorInfo();
 | 
| +        mUiHandler = new Handler();
 | 
| +
 | 
| +        mContext = Mockito.mock(Context.class);
 | 
| +        mContainerView = Mockito.mock(View.class);
 | 
| +        mImeAdapter = Mockito.mock(ImeAdapter.class);
 | 
| +        mInputMethodManager = Mockito.mock(InputMethodManager.class);
 | 
| +
 | 
| +        mFactory = new TestFactory(new InputMethodManagerWrapper(mContext));
 | 
| +
 | 
| +        when(mContext.getSystemService(Context.INPUT_METHOD_SERVICE))
 | 
| +                .thenReturn(mInputMethodManager);
 | 
| +        when(mContainerView.getContext()).thenReturn(mContext);
 | 
| +        when(mContainerView.getHandler()).thenReturn(mUiHandler);
 | 
| +        when(mContainerView.hasFocus()).thenReturn(true);
 | 
| +        when(mContainerView.hasWindowFocus()).thenReturn(true);
 | 
| +
 | 
| +        mProxyView = Mockito.mock(ThreadedInputConnectionProxyView.class);
 | 
| +        when(mProxyView.getContext()).thenReturn(mContext);
 | 
| +        when(mProxyView.requestFocus()).thenReturn(true);
 | 
| +        when(mProxyView.getHandler()).thenReturn(mImeHandler);
 | 
| +        doAnswer(new Answer<Void>() {
 | 
| +            @Override
 | 
| +            public Void answer(InvocationOnMock invocation) throws Throwable {
 | 
| +                mWindowFocusChanged = true;
 | 
| +                return null;
 | 
| +            }
 | 
| +        }).when(mProxyView).onWindowFocusChanged(true);
 | 
| +        final Callable<InputConnection> callable = new Callable<InputConnection>() {
 | 
| +            @Override
 | 
| +            public InputConnection call() throws Exception {
 | 
| +                return mFactory.initializeAndGet(
 | 
| +                        mContainerView, mImeAdapter, 1, 0, 0, 0, mEditorInfo);
 | 
| +            }
 | 
| +        };
 | 
| +        when(mProxyView.onCreateInputConnection(any(EditorInfo.class))).thenAnswer(
 | 
| +                new Answer<InputConnection>() {
 | 
| +                    @Override
 | 
| +                    public InputConnection answer(InvocationOnMock invocation) throws Throwable {
 | 
| +                        return ThreadUtils.runOnUiThreadBlockingNoException(callable);
 | 
| +                    }
 | 
| +                });
 | 
| +
 | 
| +        when(mInputMethodManager.isActive(mContainerView)).thenAnswer(new Answer<Boolean>() {
 | 
| +            private int mCount;
 | 
| +
 | 
| +            @Override
 | 
| +            public Boolean answer(InvocationOnMock invocation) throws Throwable {
 | 
| +                mCount++;
 | 
| +                if (mCount == 1 && mWindowFocusChanged) {
 | 
| +                    mInputConnection = mProxyView.onCreateInputConnection(mEditorInfo);
 | 
| +                    return false;
 | 
| +                } else if (mCount == 2) {
 | 
| +                    return true;
 | 
| +                }
 | 
| +                fail();
 | 
| +                return false;
 | 
| +            }
 | 
| +        });
 | 
| +        when(mInputMethodManager.isActive(mProxyView)).thenReturn(true);
 | 
| +
 | 
| +        mInOrder = inOrder(mImeAdapter, mInputMethodManager, mContainerView, mProxyView);
 | 
| +    }
 | 
| +
 | 
| +    private void activateInput() {
 | 
| +        mUiHandler.post(new Runnable() {
 | 
| +            @Override
 | 
| +            public void run() {
 | 
| +                assertNull(mFactory.initializeAndGet(
 | 
| +                        mContainerView, mImeAdapter, 1, 0, 0, 0, mEditorInfo));
 | 
| +            }
 | 
| +        });
 | 
| +    }
 | 
| +
 | 
| +    private void runOneUiTask() {
 | 
| +        assertTrue(Robolectric.getForegroundThreadScheduler().runOneTask());
 | 
| +    }
 | 
| +
 | 
| +    @Test
 | 
| +    @Feature({"TextInput"})
 | 
| +    public void testCreateInputConnection_Success() {
 | 
| +        // Pause all the loopers.
 | 
| +        Robolectric.getForegroundThreadScheduler().pause();
 | 
| +        mImeShadowLooper.pause();
 | 
| +
 | 
| +        activateInput();
 | 
| +
 | 
| +        // The first onCreateInputConnection().
 | 
| +        runOneUiTask();
 | 
| +        mInOrder.verify(mContainerView).hasFocus();
 | 
| +        mInOrder.verify(mContainerView).hasWindowFocus();
 | 
| +        mInOrder.verify(mProxyView).requestFocus();
 | 
| +        mInOrder.verify(mContainerView).getHandler();
 | 
| +        mInOrder.verifyNoMoreInteractions();
 | 
| +        assertNull(mInputConnection);
 | 
| +
 | 
| +        // The second onCreateInputConnection().
 | 
| +        runOneUiTask();
 | 
| +        mInOrder.verify(mProxyView).onWindowFocusChanged(true);
 | 
| +        mInOrder.verify(mInputMethodManager).isActive(mContainerView);
 | 
| +        mInOrder.verify(mProxyView).onCreateInputConnection(any(EditorInfo.class));
 | 
| +        mInOrder.verify(mContainerView).getContext();  // BaseInputConnection#<init>
 | 
| +        mInOrder.verifyNoMoreInteractions();
 | 
| +        assertNotNull(mInputConnection);
 | 
| +        assertTrue(ThreadedInputConnection.class.isInstance(mInputConnection));
 | 
| +
 | 
| +        // Verification process.
 | 
| +        mImeShadowLooper.runOneTask();
 | 
| +        runOneUiTask();
 | 
| +
 | 
| +        mInOrder.verify(mInputMethodManager).isActive(mProxyView);
 | 
| +        mInOrder.verifyNoMoreInteractions();
 | 
| +
 | 
| +        assertTrue(mFactory.hasSucceeded());
 | 
| +        assertFalse(mFactory.hasFailed());
 | 
| +    }
 | 
| +
 | 
| +    @Test
 | 
| +    @Feature({"TextInput"})
 | 
| +    public void testCreateInputConnection_WindowFocusLostOnFirstLoop() {
 | 
| +        // Somehow input was activated right after window focus was lost.
 | 
| +        when(mContainerView.hasWindowFocus()).thenReturn(false);
 | 
| +
 | 
| +        // Pause all the loopers.
 | 
| +        Robolectric.getForegroundThreadScheduler().pause();
 | 
| +        mImeShadowLooper.pause();
 | 
| +
 | 
| +        activateInput();
 | 
| +
 | 
| +        // The first onCreateInputConnection().
 | 
| +        runOneUiTask();
 | 
| +        mInOrder.verify(mContainerView).hasFocus();
 | 
| +        mInOrder.verify(mContainerView).hasWindowFocus();
 | 
| +        mInOrder.verifyNoMoreInteractions();
 | 
| +        assertNull(mInputConnection);
 | 
| +        assertFalse(mFactory.hasSucceeded());
 | 
| +        assertFalse(mFactory.hasFailed());
 | 
| +    }
 | 
| +
 | 
| +    @Test
 | 
| +    @Feature({"TextInput"})
 | 
| +    public void testCreateInputConnection_WindowFocusLostOnSecondLoop() {
 | 
| +        // Pause all the loopers.
 | 
| +        Robolectric.getForegroundThreadScheduler().pause();
 | 
| +        mImeShadowLooper.pause();
 | 
| +
 | 
| +        activateInput();
 | 
| +
 | 
| +        // The first onCreateInputConnection().
 | 
| +        runOneUiTask();
 | 
| +        mInOrder.verify(mContainerView).hasFocus();
 | 
| +        mInOrder.verify(mContainerView).hasWindowFocus();
 | 
| +        mInOrder.verify(mProxyView).requestFocus();
 | 
| +        mInOrder.verify(mContainerView).getHandler();
 | 
| +        mInOrder.verifyNoMoreInteractions();
 | 
| +        assertNull(mInputConnection);
 | 
| +
 | 
| +        // Now window focus was lost before the second onCreateInputConnection().
 | 
| +        mFactory.onWindowFocusChanged(false);
 | 
| +
 | 
| +        // The second onCreateInputConnection() gets ignored.
 | 
| +        runOneUiTask();
 | 
| +        mInOrder.verify(mProxyView).onOriginalViewWindowFocusChanged(false);
 | 
| +        mInOrder.verifyNoMoreInteractions();
 | 
| +        assertNull(mInputConnection);
 | 
| +        assertFalse(mFactory.hasSucceeded());
 | 
| +        assertFalse(mFactory.hasFailed());
 | 
| +    }
 | 
| +}
 | 
| 
 |