| package io.flutter.plugin.editing; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.assertTrue; |
| import static org.mockito.Mockito.anyString; |
| import static org.mockito.Mockito.eq; |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.spy; |
| import static org.mockito.Mockito.times; |
| import static org.mockito.Mockito.verify; |
| |
| import android.content.ClipboardManager; |
| import android.content.res.AssetManager; |
| import android.text.Editable; |
| import android.text.InputType; |
| import android.text.Selection; |
| import android.text.SpannableStringBuilder; |
| import android.view.KeyEvent; |
| import android.view.View; |
| import android.view.inputmethod.EditorInfo; |
| import android.view.inputmethod.ExtractedText; |
| import io.flutter.embedding.engine.FlutterJNI; |
| import io.flutter.embedding.engine.dart.DartExecutor; |
| import io.flutter.embedding.engine.systemchannels.TextInputChannel; |
| import io.flutter.util.FakeKeyEvent; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.robolectric.RobolectricTestRunner; |
| import org.robolectric.RuntimeEnvironment; |
| import org.robolectric.annotation.Config; |
| import org.robolectric.shadows.ShadowClipboardManager; |
| |
| @Config(manifest = Config.NONE, sdk = 27, shadows = ShadowClipboardManager.class) |
| @RunWith(RobolectricTestRunner.class) |
| public class InputConnectionAdaptorTest { |
| @Test |
| public void inputConnectionAdaptor_ReceivesEnter() throws NullPointerException { |
| View testView = new View(RuntimeEnvironment.application); |
| FlutterJNI mockFlutterJni = mock(FlutterJNI.class); |
| DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class))); |
| int inputTargetId = 0; |
| TextInputChannel textInputChannel = new TextInputChannel(dartExecutor); |
| Editable mEditable = Editable.Factory.getInstance().newEditable(""); |
| Editable spyEditable = spy(mEditable); |
| EditorInfo outAttrs = new EditorInfo(); |
| outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE; |
| |
| InputConnectionAdaptor inputConnectionAdaptor = |
| new InputConnectionAdaptor( |
| testView, inputTargetId, textInputChannel, spyEditable, outAttrs); |
| |
| // Send an enter key and make sure the Editable received it. |
| FakeKeyEvent keyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER); |
| inputConnectionAdaptor.sendKeyEvent(keyEvent); |
| verify(spyEditable, times(1)).insert(eq(0), anyString()); |
| } |
| |
| @Test |
| public void testPerformContextMenuAction_selectAll() { |
| int selStart = 5; |
| Editable editable = sampleEditable(selStart, selStart); |
| InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); |
| |
| boolean didConsume = adaptor.performContextMenuAction(android.R.id.selectAll); |
| |
| assertTrue(didConsume); |
| assertEquals(0, Selection.getSelectionStart(editable)); |
| assertEquals(editable.length(), Selection.getSelectionEnd(editable)); |
| } |
| |
| @Test |
| public void testPerformContextMenuAction_cut() { |
| ClipboardManager clipboardManager = |
| RuntimeEnvironment.application.getSystemService(ClipboardManager.class); |
| int selStart = 6; |
| int selEnd = 11; |
| Editable editable = sampleEditable(selStart, selEnd); |
| CharSequence textToBeCut = editable.subSequence(selStart, selEnd); |
| InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); |
| |
| boolean didConsume = adaptor.performContextMenuAction(android.R.id.cut); |
| |
| assertTrue(didConsume); |
| assertTrue(clipboardManager.hasText()); |
| assertEquals(textToBeCut, clipboardManager.getPrimaryClip().getItemAt(0).getText()); |
| assertFalse(editable.toString().contains(textToBeCut)); |
| } |
| |
| @Test |
| public void testPerformContextMenuAction_copy() { |
| ClipboardManager clipboardManager = |
| RuntimeEnvironment.application.getSystemService(ClipboardManager.class); |
| int selStart = 6; |
| int selEnd = 11; |
| Editable editable = sampleEditable(selStart, selEnd); |
| InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); |
| |
| assertFalse(clipboardManager.hasText()); |
| |
| boolean didConsume = adaptor.performContextMenuAction(android.R.id.copy); |
| |
| assertTrue(didConsume); |
| assertTrue(clipboardManager.hasText()); |
| assertEquals( |
| editable.subSequence(selStart, selEnd), |
| clipboardManager.getPrimaryClip().getItemAt(0).getText()); |
| } |
| |
| @Test |
| public void testPerformContextMenuAction_paste() { |
| ClipboardManager clipboardManager = |
| RuntimeEnvironment.application.getSystemService(ClipboardManager.class); |
| String textToBePasted = "deadbeef"; |
| clipboardManager.setText(textToBePasted); |
| Editable editable = sampleEditable(0, 0); |
| InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); |
| |
| boolean didConsume = adaptor.performContextMenuAction(android.R.id.paste); |
| |
| assertTrue(didConsume); |
| assertTrue(editable.toString().startsWith(textToBePasted)); |
| } |
| |
| @Test |
| public void testSendKeyEvent_shiftKeyUpCancelsSelection() { |
| int selStart = 5; |
| int selEnd = 10; |
| Editable editable = sampleEditable(selStart, selEnd); |
| InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); |
| |
| KeyEvent shiftKeyUp = new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SHIFT_LEFT); |
| boolean didConsume = adaptor.sendKeyEvent(shiftKeyUp); |
| |
| assertTrue(didConsume); |
| assertEquals(selEnd, Selection.getSelectionStart(editable)); |
| assertEquals(selEnd, Selection.getSelectionEnd(editable)); |
| } |
| |
| @Test |
| public void testSendKeyEvent_leftKeyMovesCaretLeft() { |
| int selStart = 5; |
| Editable editable = sampleEditable(selStart, selStart); |
| InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); |
| |
| KeyEvent leftKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT); |
| boolean didConsume = adaptor.sendKeyEvent(leftKeyDown); |
| |
| assertTrue(didConsume); |
| assertEquals(selStart - 1, Selection.getSelectionStart(editable)); |
| assertEquals(selStart - 1, Selection.getSelectionEnd(editable)); |
| } |
| |
| @Test |
| public void testSendKeyEvent_leftKeyExtendsSelectionLeft() { |
| int selStart = 5; |
| int selEnd = 40; |
| Editable editable = sampleEditable(selStart, selEnd); |
| InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); |
| |
| KeyEvent leftKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT); |
| boolean didConsume = adaptor.sendKeyEvent(leftKeyDown); |
| |
| assertTrue(didConsume); |
| assertEquals(selStart, Selection.getSelectionStart(editable)); |
| assertEquals(selEnd - 1, Selection.getSelectionEnd(editable)); |
| } |
| |
| @Test |
| public void testSendKeyEvent_shiftLeftKeyStartsSelectionLeft() { |
| int selStart = 5; |
| Editable editable = sampleEditable(selStart, selStart); |
| InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); |
| |
| KeyEvent shiftLeftKeyDown = |
| new KeyEvent( |
| 0, 0, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT, 0, KeyEvent.META_SHIFT_ON); |
| boolean didConsume = adaptor.sendKeyEvent(shiftLeftKeyDown); |
| |
| assertTrue(didConsume); |
| assertEquals(selStart, Selection.getSelectionStart(editable)); |
| assertEquals(selStart - 1, Selection.getSelectionEnd(editable)); |
| } |
| |
| @Test |
| public void testSendKeyEvent_rightKeyMovesCaretRight() { |
| int selStart = 5; |
| Editable editable = sampleEditable(selStart, selStart); |
| InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); |
| |
| KeyEvent rightKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT); |
| boolean didConsume = adaptor.sendKeyEvent(rightKeyDown); |
| |
| assertTrue(didConsume); |
| assertEquals(selStart + 1, Selection.getSelectionStart(editable)); |
| assertEquals(selStart + 1, Selection.getSelectionEnd(editable)); |
| } |
| |
| @Test |
| public void testSendKeyEvent_rightKeyExtendsSelectionRight() { |
| int selStart = 5; |
| int selEnd = 40; |
| Editable editable = sampleEditable(selStart, selEnd); |
| InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); |
| |
| KeyEvent rightKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT); |
| boolean didConsume = adaptor.sendKeyEvent(rightKeyDown); |
| |
| assertTrue(didConsume); |
| assertEquals(selStart, Selection.getSelectionStart(editable)); |
| assertEquals(selEnd + 1, Selection.getSelectionEnd(editable)); |
| } |
| |
| @Test |
| public void testSendKeyEvent_shiftRightKeyStartsSelectionRight() { |
| int selStart = 5; |
| Editable editable = sampleEditable(selStart, selStart); |
| InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); |
| |
| KeyEvent shiftRightKeyDown = |
| new KeyEvent( |
| 0, 0, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT, 0, KeyEvent.META_SHIFT_ON); |
| boolean didConsume = adaptor.sendKeyEvent(shiftRightKeyDown); |
| |
| assertTrue(didConsume); |
| assertEquals(selStart, Selection.getSelectionStart(editable)); |
| assertEquals(selStart + 1, Selection.getSelectionEnd(editable)); |
| } |
| |
| @Test |
| public void testSendKeyEvent_upKeyMovesCaretUp() { |
| int selStart = SAMPLE_TEXT.indexOf('\n') + 4; |
| Editable editable = sampleEditable(selStart, selStart); |
| InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); |
| |
| KeyEvent upKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_UP); |
| boolean didConsume = adaptor.sendKeyEvent(upKeyDown); |
| |
| assertTrue(didConsume); |
| // Checks the caret moved left (to some previous character). Selection.moveUp() behaves |
| // different in tests than on a real device, we can't verify the exact position. |
| assertTrue(Selection.getSelectionStart(editable) < selStart); |
| } |
| |
| @Test |
| public void testSendKeyEvent_downKeyMovesCaretDown() { |
| int selStart = 4; |
| Editable editable = sampleEditable(selStart, selStart); |
| InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); |
| |
| KeyEvent downKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_DOWN); |
| boolean didConsume = adaptor.sendKeyEvent(downKeyDown); |
| |
| assertTrue(didConsume); |
| // Checks the caret moved right (to some following character). Selection.moveDown() behaves |
| // different in tests than on a real device, we can't verify the exact position. |
| assertTrue(Selection.getSelectionStart(editable) > selStart); |
| } |
| |
| @Test |
| public void testMethod_getExtractedText() { |
| int selStart = 5; |
| Editable editable = sampleEditable(selStart, selStart); |
| InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); |
| |
| ExtractedText extractedText = adaptor.getExtractedText(null, 0); |
| |
| assertEquals(extractedText.text, SAMPLE_TEXT); |
| assertEquals(extractedText.selectionStart, selStart); |
| assertEquals(extractedText.selectionEnd, selStart); |
| } |
| |
| @Test |
| public void inputConnectionAdaptor_RepeatFilter() throws NullPointerException { |
| View testView = new View(RuntimeEnvironment.application); |
| FlutterJNI mockFlutterJni = mock(FlutterJNI.class); |
| DartExecutor dartExecutor = spy(new DartExecutor(mockFlutterJni, mock(AssetManager.class))); |
| int inputTargetId = 0; |
| TestTextInputChannel textInputChannel = new TestTextInputChannel(dartExecutor); |
| Editable mEditable = Editable.Factory.getInstance().newEditable(""); |
| Editable spyEditable = spy(mEditable); |
| EditorInfo outAttrs = new EditorInfo(); |
| outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE; |
| |
| InputConnectionAdaptor inputConnectionAdaptor = |
| new InputConnectionAdaptor( |
| testView, inputTargetId, textInputChannel, spyEditable, outAttrs); |
| |
| inputConnectionAdaptor.beginBatchEdit(); |
| assertEquals(textInputChannel.updateEditingStateInvocations, 0); |
| inputConnectionAdaptor.setComposingText("I do not fear computers. I fear the lack of them.", 1); |
| assertEquals(textInputChannel.text, null); |
| assertEquals(textInputChannel.updateEditingStateInvocations, 0); |
| inputConnectionAdaptor.endBatchEdit(); |
| assertEquals(textInputChannel.updateEditingStateInvocations, 1); |
| assertEquals(textInputChannel.text, "I do not fear computers. I fear the lack of them."); |
| |
| inputConnectionAdaptor.beginBatchEdit(); |
| assertEquals(textInputChannel.updateEditingStateInvocations, 1); |
| inputConnectionAdaptor.endBatchEdit(); |
| assertEquals(textInputChannel.updateEditingStateInvocations, 1); |
| |
| inputConnectionAdaptor.beginBatchEdit(); |
| assertEquals(textInputChannel.text, "I do not fear computers. I fear the lack of them."); |
| assertEquals(textInputChannel.updateEditingStateInvocations, 1); |
| inputConnectionAdaptor.setSelection(3, 4); |
| assertEquals(textInputChannel.updateEditingStateInvocations, 1); |
| assertEquals(textInputChannel.selectionStart, 49); |
| assertEquals(textInputChannel.selectionEnd, 49); |
| inputConnectionAdaptor.endBatchEdit(); |
| assertEquals(textInputChannel.updateEditingStateInvocations, 2); |
| assertEquals(textInputChannel.selectionStart, 3); |
| assertEquals(textInputChannel.selectionEnd, 4); |
| } |
| |
| @Test |
| public void testSendKeyEvent_delKeyDeletesBackward() { |
| int selStart = 29; |
| Editable editable = sampleRtlEditable(selStart, selStart); |
| InputConnectionAdaptor adaptor = sampleInputConnectionAdaptor(editable); |
| |
| KeyEvent downKeyDown = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL); |
| |
| for (int i = 0; i < 9; i++) { |
| boolean didConsume = adaptor.sendKeyEvent(downKeyDown); |
| assertTrue(didConsume); |
| } |
| assertEquals(Selection.getSelectionStart(editable), 19); |
| |
| for (int i = 0; i < 9; i++) { |
| boolean didConsume = adaptor.sendKeyEvent(downKeyDown); |
| assertTrue(didConsume); |
| } |
| assertEquals(Selection.getSelectionStart(editable), 10); |
| } |
| |
| private static final String SAMPLE_TEXT = |
| "Lorem ipsum dolor sit amet," + "\nconsectetur adipiscing elit."; |
| |
| private static final String SAMPLE_RTL_TEXT = "متن ساختگی" + "\nبرای تستfor test😊"; |
| |
| private static Editable sampleEditable(int selStart, int selEnd) { |
| SpannableStringBuilder sample = new SpannableStringBuilder(SAMPLE_TEXT); |
| Selection.setSelection(sample, selStart, selEnd); |
| return sample; |
| } |
| |
| private static Editable sampleRtlEditable(int selStart, int selEnd) { |
| SpannableStringBuilder sample = new SpannableStringBuilder(SAMPLE_RTL_TEXT); |
| Selection.setSelection(sample, selStart, selEnd); |
| return sample; |
| } |
| |
| private static InputConnectionAdaptor sampleInputConnectionAdaptor(Editable editable) { |
| View testView = new View(RuntimeEnvironment.application); |
| int client = 0; |
| TextInputChannel textInputChannel = mock(TextInputChannel.class); |
| return new InputConnectionAdaptor(testView, client, textInputChannel, editable, null); |
| } |
| |
| private class TestTextInputChannel extends TextInputChannel { |
| public TestTextInputChannel(DartExecutor dartExecutor) { |
| super(dartExecutor); |
| } |
| |
| public int inputClientId; |
| public String text; |
| public int selectionStart; |
| public int selectionEnd; |
| public int composingStart; |
| public int composingEnd; |
| public int updateEditingStateInvocations = 0; |
| |
| @Override |
| public void updateEditingState( |
| int inputClientId, |
| String text, |
| int selectionStart, |
| int selectionEnd, |
| int composingStart, |
| int composingEnd) { |
| this.inputClientId = inputClientId; |
| this.text = text; |
| this.selectionStart = selectionStart; |
| this.selectionEnd = selectionEnd; |
| this.composingStart = composingStart; |
| this.composingEnd = composingEnd; |
| updateEditingStateInvocations++; |
| } |
| } |
| } |