1 /*
<lambda>null2 * Copyright 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.compose.foundation.text.input
18
19 import android.R
20 import android.os.Build
21 import android.text.InputType
22 import android.text.SpannableStringBuilder
23 import android.text.style.BackgroundColorSpan
24 import android.view.inputmethod.EditorInfo
25 import android.view.inputmethod.InputConnection
26 import androidx.compose.foundation.ExperimentalFoundationApi
27 import androidx.compose.foundation.ScrollState
28 import androidx.compose.foundation.focusable
29 import androidx.compose.foundation.internal.readAnnotatedString
30 import androidx.compose.foundation.internal.readText
31 import androidx.compose.foundation.internal.toClipEntry
32 import androidx.compose.foundation.layout.Box
33 import androidx.compose.foundation.layout.Column
34 import androidx.compose.foundation.layout.Row
35 import androidx.compose.foundation.layout.fillMaxSize
36 import androidx.compose.foundation.layout.height
37 import androidx.compose.foundation.layout.size
38 import androidx.compose.foundation.layout.width
39 import androidx.compose.foundation.text.BasicTextField
40 import androidx.compose.foundation.text.KeyboardOptions
41 import androidx.compose.foundation.text.TEST_FONT_FAMILY
42 import androidx.compose.foundation.text.computeSizeForDefaultText
43 import androidx.compose.foundation.text.input.TextFieldBuffer.ChangeList
44 import androidx.compose.foundation.text.input.internal.TextLayoutState
45 import androidx.compose.foundation.text.input.internal.TransformedTextFieldState
46 import androidx.compose.foundation.text.input.internal.selection.FakeClipboard
47 import androidx.compose.foundation.text.input.internal.selection.TextFieldSelectionState
48 import androidx.compose.foundation.text.selection.fetchTextLayoutResult
49 import androidx.compose.foundation.verticalScroll
50 import androidx.compose.runtime.CompositionLocalProvider
51 import androidx.compose.runtime.LaunchedEffect
52 import androidx.compose.runtime.derivedStateOf
53 import androidx.compose.runtime.getValue
54 import androidx.compose.runtime.mutableStateOf
55 import androidx.compose.runtime.remember
56 import androidx.compose.runtime.setValue
57 import androidx.compose.runtime.snapshotFlow
58 import androidx.compose.testutils.assertPixelColor
59 import androidx.compose.ui.ExperimentalComposeUiApi
60 import androidx.compose.ui.Modifier
61 import androidx.compose.ui.focus.FocusDirection
62 import androidx.compose.ui.focus.FocusManager
63 import androidx.compose.ui.focus.FocusRequester
64 import androidx.compose.ui.focus.focusRequester
65 import androidx.compose.ui.graphics.Color
66 import androidx.compose.ui.graphics.ImageBitmap
67 import androidx.compose.ui.graphics.toPixelMap
68 import androidx.compose.ui.input.key.Key
69 import androidx.compose.ui.platform.ClipEntry
70 import androidx.compose.ui.platform.Clipboard
71 import androidx.compose.ui.platform.InterceptPlatformTextInput
72 import androidx.compose.ui.platform.LocalClipboard
73 import androidx.compose.ui.platform.LocalDensity
74 import androidx.compose.ui.platform.LocalFocusManager
75 import androidx.compose.ui.platform.LocalFontFamilyResolver
76 import androidx.compose.ui.platform.LocalSoftwareKeyboardController
77 import androidx.compose.ui.platform.LocalWindowInfo
78 import androidx.compose.ui.platform.NativeClipboard
79 import androidx.compose.ui.platform.WindowInfo
80 import androidx.compose.ui.platform.testTag
81 import androidx.compose.ui.semantics.SemanticsActions
82 import androidx.compose.ui.semantics.SemanticsProperties.TextSelectionRange
83 import androidx.compose.ui.semantics.getOrNull
84 import androidx.compose.ui.test.ExperimentalTestApi
85 import androidx.compose.ui.test.assertIsFocused
86 import androidx.compose.ui.test.assertIsNotEnabled
87 import androidx.compose.ui.test.assertIsNotFocused
88 import androidx.compose.ui.test.assertTextEquals
89 import androidx.compose.ui.test.captureToImage
90 import androidx.compose.ui.test.hasPerformImeAction
91 import androidx.compose.ui.test.junit4.createComposeRule
92 import androidx.compose.ui.test.longClick
93 import androidx.compose.ui.test.onNodeWithTag
94 import androidx.compose.ui.test.performClick
95 import androidx.compose.ui.test.performKeyInput
96 import androidx.compose.ui.test.performSemanticsAction
97 import androidx.compose.ui.test.performTextInput
98 import androidx.compose.ui.test.performTextInputSelection
99 import androidx.compose.ui.test.performTextReplacement
100 import androidx.compose.ui.test.performTouchInput
101 import androidx.compose.ui.test.pressKey
102 import androidx.compose.ui.test.requestFocus
103 import androidx.compose.ui.test.swipeRight
104 import androidx.compose.ui.test.swipeUp
105 import androidx.compose.ui.text.AnnotatedString
106 import androidx.compose.ui.text.SpanStyle
107 import androidx.compose.ui.text.TextLayoutResult
108 import androidx.compose.ui.text.TextRange
109 import androidx.compose.ui.text.TextStyle
110 import androidx.compose.ui.text.input.ImeAction
111 import androidx.compose.ui.text.input.KeyboardCapitalization
112 import androidx.compose.ui.text.input.KeyboardType
113 import androidx.compose.ui.text.intl.Locale
114 import androidx.compose.ui.text.intl.LocaleList
115 import androidx.compose.ui.text.style.TextAlign
116 import androidx.compose.ui.text.style.TextDecoration
117 import androidx.compose.ui.text.style.TextDirection
118 import androidx.compose.ui.unit.Density
119 import androidx.compose.ui.unit.dp
120 import androidx.compose.ui.unit.sp
121 import androidx.core.text.toSpanned
122 import androidx.test.ext.junit.runners.AndroidJUnit4
123 import androidx.test.filters.LargeTest
124 import androidx.test.filters.SdkSuppress
125 import com.google.common.truth.Truth.assertThat
126 import kotlin.test.assertNotNull
127 import kotlinx.coroutines.awaitCancellation
128 import kotlinx.coroutines.flow.drop
129 import kotlinx.coroutines.test.runTest
130 import org.junit.Rule
131 import org.junit.Test
132 import org.junit.runner.RunWith
133 import org.mockito.kotlin.mock
134 import org.mockito.kotlin.times
135 import org.mockito.kotlin.verify
136
137 @OptIn(ExperimentalFoundationApi::class, ExperimentalTestApi::class)
138 @LargeTest
139 @RunWith(AndroidJUnit4::class)
140 internal class BasicTextFieldTest {
141 @get:Rule val rule = createComposeRule()
142
143 @get:Rule val immRule = ComposeInputMethodManagerTestRule()
144
145 private val inputMethodInterceptor = InputMethodInterceptor(rule)
146
147 private val Tag = "BasicTextField"
148
149 private val imm = FakeInputMethodManager()
150
151 @Test
152 fun textField_rendersEmptyContent() {
153 var textLayoutResult: (() -> TextLayoutResult?)? = null
154 inputMethodInterceptor.setTextFieldTestContent {
155 val state = remember { TextFieldState() }
156 BasicTextField(
157 state = state,
158 modifier = Modifier.fillMaxSize(),
159 onTextLayout = { textLayoutResult = it }
160 )
161 }
162
163 rule.runOnIdle {
164 assertThat(textLayoutResult).isNotNull()
165 assertThat(textLayoutResult?.invoke()?.layoutInput?.text).isEqualTo(AnnotatedString(""))
166 }
167 }
168
169 @Test
170 fun textFieldState_textChange_updatesState() {
171 val state = TextFieldState("Hello ", TextRange(Int.MAX_VALUE))
172 inputMethodInterceptor.setTextFieldTestContent {
173 BasicTextField(state = state, modifier = Modifier.fillMaxSize().testTag(Tag))
174 }
175
176 rule.onNodeWithTag(Tag).performTextInput("World!")
177
178 rule.runOnIdle { assertThat(state.text.toString()).isEqualTo("Hello World!") }
179 }
180
181 @Test
182 fun textFieldState_textChange_updatesSemantics() {
183 val state = TextFieldState("Hello ", TextRange(Int.MAX_VALUE))
184 inputMethodInterceptor.setTextFieldTestContent {
185 BasicTextField(state = state, modifier = Modifier.fillMaxSize().testTag(Tag))
186 }
187
188 rule.onNodeWithTag(Tag).performTextInput("World!")
189
190 rule.onNodeWithTag(Tag).assertTextEquals("Hello World!")
191 assertTextSelection(TextRange("Hello World!".length))
192 }
193
194 /**
195 * This is a goal that we set for ourselves. Only updating the editing buffer should not cause
196 * BasicTextField to recompose.
197 */
198 @Test
199 fun textField_imeUpdatesDontCauseRecomposition() {
200 val state = TextFieldState()
201 var compositionCount = 0
202 inputMethodInterceptor.setTextFieldTestContent {
203 compositionCount++
204 BasicTextField(
205 state = state,
206 modifier = Modifier.fillMaxSize().testTag(Tag),
207 )
208 }
209
210 rule.onNodeWithTag(Tag).performTextInput("hello")
211 rule.onNodeWithTag(Tag).performTextInput("world")
212
213 rule.onNodeWithTag(Tag).assertTextEquals("helloworld")
214 rule.runOnIdle { assertThat(compositionCount).isEqualTo(1) }
215 }
216
217 @Test
218 fun textField_textStyleFontSizeChange_relayouts() {
219 val state = TextFieldState("Hello ", TextRange(Int.MAX_VALUE))
220 var style by mutableStateOf(TextStyle(fontSize = 20.sp))
221 var textLayoutResultState: (() -> TextLayoutResult?)? by mutableStateOf(null)
222 val textLayoutResults = mutableListOf<TextLayoutResult?>()
223 inputMethodInterceptor.setTextFieldTestContent {
224 BasicTextField(
225 state = state,
226 modifier = Modifier.fillMaxSize().testTag(Tag),
227 textStyle = style,
228 onTextLayout = { textLayoutResultState = it }
229 )
230
231 LaunchedEffect(Unit) {
232 snapshotFlow { textLayoutResultState?.invoke() }
233 .drop(1)
234 .collect { textLayoutResults += it }
235 }
236 }
237
238 style = TextStyle(fontSize = 30.sp)
239
240 rule.runOnIdle {
241 assertThat(textLayoutResults.size).isEqualTo(2)
242 assertThat(textLayoutResults.map { it?.layoutInput?.style?.fontSize })
243 .containsExactly(20.sp, 30.sp)
244 .inOrder()
245 }
246 }
247
248 @Test
249 fun textField_textStyleColorChange_doesNotRelayout() {
250 val state = TextFieldState("Hello")
251 var style by mutableStateOf(TextStyle(color = Color.Red))
252 var textLayoutResultState: (() -> TextLayoutResult?)? by mutableStateOf(null)
253 val textLayoutResults = mutableListOf<TextLayoutResult?>()
254 inputMethodInterceptor.setTextFieldTestContent {
255 BasicTextField(
256 state = state,
257 modifier = Modifier.fillMaxSize().testTag(Tag),
258 textStyle = style,
259 onTextLayout = { textLayoutResultState = it }
260 )
261
262 LaunchedEffect(Unit) {
263 snapshotFlow { textLayoutResultState?.invoke() }
264 .drop(1)
265 .collect { textLayoutResults += it }
266 }
267 }
268
269 style = TextStyle(color = Color.Blue)
270
271 rule.runOnIdle {
272 assertThat(textLayoutResults.size).isEqualTo(2)
273 assertThat(textLayoutResults[0]?.multiParagraph)
274 .isSameInstanceAs(textLayoutResults[1]?.multiParagraph)
275 assertThat(textLayoutResults[0]?.layoutInput?.style?.color).isEqualTo(Color.Red)
276 assertThat(textLayoutResults[1]?.layoutInput?.style?.color).isEqualTo(Color.Blue)
277 }
278 }
279
280 @Test
281 fun textField_contentChange_relayouts() {
282 val state = TextFieldState("Hello ", TextRange(Int.MAX_VALUE))
283 var textLayoutResultState: (() -> TextLayoutResult?)? by mutableStateOf(null)
284 val textLayoutResults = mutableListOf<TextLayoutResult?>()
285 inputMethodInterceptor.setTextFieldTestContent {
286 BasicTextField(
287 state = state,
288 modifier = Modifier.fillMaxSize().testTag(Tag),
289 onTextLayout = { textLayoutResultState = it }
290 )
291
292 LaunchedEffect(Unit) {
293 snapshotFlow { textLayoutResultState?.invoke() }
294 .drop(1)
295 .collect { textLayoutResults += it }
296 }
297 }
298
299 rule.onNodeWithTag(Tag).performTextInput("World!")
300
301 rule.runOnIdle {
302 assertThat(textLayoutResults.map { it?.layoutInput?.text?.text })
303 .containsExactly("Hello ", "Hello World!")
304 .inOrder()
305 }
306 }
307
308 @Test
309 fun textField_focus_showsSoftwareKeyboard() {
310 val state = TextFieldState()
311 inputMethodInterceptor.setTextFieldTestContent {
312 BasicTextField(state = state, modifier = Modifier.fillMaxSize().testTag(Tag))
313 }
314
315 rule.onNodeWithTag(Tag).performClick()
316 rule.onNodeWithTag(Tag).assertIsFocused()
317
318 inputMethodInterceptor.assertSessionActive()
319 }
320
321 @Test
322 fun textField_focus_doesNotShowSoftwareKeyboard_ifDisabled() {
323 val state = TextFieldState()
324 inputMethodInterceptor.setTextFieldTestContent {
325 BasicTextField(
326 state = state,
327 enabled = false,
328 modifier = Modifier.fillMaxSize().testTag(Tag)
329 )
330 }
331
332 rule.onNodeWithTag(Tag).assertIsNotEnabled()
333 rule.onNodeWithTag(Tag).performClick()
334
335 inputMethodInterceptor.assertNoSessionActive()
336 }
337
338 @Test
339 fun textField_focus_doesNotShowSoftwareKeyboard_ifReadOnly() {
340 val state = TextFieldState()
341 inputMethodInterceptor.setTextFieldTestContent {
342 BasicTextField(
343 state = state,
344 readOnly = true,
345 modifier = Modifier.fillMaxSize().testTag(Tag)
346 )
347 }
348
349 rule.onNodeWithTag(Tag).performClick()
350 rule.onNodeWithTag(Tag).assertIsFocused()
351
352 inputMethodInterceptor.assertNoSessionActive()
353 }
354
355 @Test
356 fun textField_focus_doesNotShowSoftwareKeyboard_whenNotShowSoftwareKeyboard() {
357 val state = TextFieldState()
358 val focusRequester = FocusRequester()
359 inputMethodInterceptor.setTextFieldTestContent {
360 BasicTextField(
361 state = state,
362 keyboardOptions = KeyboardOptions(showKeyboardOnFocus = false),
363 modifier = Modifier.fillMaxSize().testTag(Tag).focusRequester(focusRequester)
364 )
365 }
366 rule.runOnUiThread { focusRequester.requestFocus() }
367 rule.waitForIdle()
368 rule.onNodeWithTag(Tag).assertIsFocused()
369
370 inputMethodInterceptor.assertNoSessionActive()
371 }
372
373 @Test
374 fun textField_tap_showSoftwareKeyboard_whenNotShowSoftwareKeyboard() {
375 val state = TextFieldState()
376 val focusRequester = FocusRequester()
377 inputMethodInterceptor.setTextFieldTestContent {
378 BasicTextField(
379 state = state,
380 keyboardOptions = KeyboardOptions(showKeyboardOnFocus = false),
381 modifier = Modifier.fillMaxSize().testTag(Tag).focusRequester(focusRequester)
382 )
383 }
384 rule.runOnUiThread { focusRequester.requestFocus() }
385 rule.waitForIdle()
386 rule.onNodeWithTag(Tag).assertIsFocused()
387 rule.onNodeWithTag(Tag).performClick()
388
389 inputMethodInterceptor.assertSessionActive()
390 }
391
392 @Test
393 fun disposeSession_whenTextFieldIsRemoved() {
394 val state = TextFieldState("initial text")
395 var toggle by mutableStateOf(true)
396 inputMethodInterceptor.setContent {
397 if (toggle) {
398 BasicTextField(state = state, modifier = Modifier.testTag("TextField"))
399 }
400 }
401
402 rule.onNodeWithTag("TextField").requestFocus()
403 inputMethodInterceptor.assertSessionActive()
404
405 toggle = false
406
407 inputMethodInterceptor.assertNoSessionActive()
408 }
409
410 @Test
411 fun disposeSessionWhenFocusCleared() {
412 val state = TextFieldState("initial text")
413 lateinit var focusManager: FocusManager
414 inputMethodInterceptor.setContent {
415 focusManager = LocalFocusManager.current
416 Row {
417 // Extra focusable that takes initial focus when focus is cleared.
418 Box(Modifier.size(10.dp).focusable())
419 BasicTextField(state = state, modifier = Modifier.testTag("TextField"))
420 }
421 }
422
423 rule.onNodeWithTag("TextField").requestFocus()
424
425 inputMethodInterceptor.assertSessionActive()
426
427 rule.runOnIdle { focusManager.clearFocus() }
428
429 inputMethodInterceptor.assertNoSessionActive()
430 }
431
432 @Test
433 fun textField_whenStateObjectChanges_newTextIsRendered() {
434 val state1 = TextFieldState("Hello")
435 val state2 = TextFieldState("World")
436 var toggleState by mutableStateOf(true)
437 val state by derivedStateOf { if (toggleState) state1 else state2 }
438 inputMethodInterceptor.setTextFieldTestContent {
439 BasicTextField(
440 state = state,
441 enabled = true,
442 modifier = Modifier.fillMaxSize().testTag(Tag)
443 )
444 }
445
446 rule.onNodeWithTag(Tag).assertTextEquals("Hello")
447 toggleState = !toggleState
448 rule.onNodeWithTag(Tag).assertTextEquals("World")
449 }
450
451 @Test
452 fun textField_whenStateObjectChanges_restartsInput() {
453 val state1 = TextFieldState("Hello")
454 val state2 = TextFieldState("World")
455 var toggleState by mutableStateOf(true)
456 val state by derivedStateOf { if (toggleState) state1 else state2 }
457 inputMethodInterceptor.setTextFieldTestContent {
458 BasicTextField(
459 state = state,
460 enabled = true,
461 modifier = Modifier.fillMaxSize().testTag(Tag)
462 )
463 }
464
465 with(rule.onNodeWithTag(Tag)) {
466 performTextReplacement("Compose")
467 assertTextEquals("Compose")
468 }
469 toggleState = !toggleState
470 with(rule.onNodeWithTag(Tag)) {
471 performTextReplacement("Compose2")
472 assertTextEquals("Compose2")
473 }
474 assertThat(state1.text.toString()).isEqualTo("Compose")
475 assertThat(state2.text.toString()).isEqualTo("Compose2")
476 }
477
478 @Test
479 fun textField_passesKeyboardOptionsThrough() {
480 val state = TextFieldState()
481 inputMethodInterceptor.setTextFieldTestContent {
482 BasicTextField(
483 state = state,
484 modifier = Modifier.testTag(Tag),
485 // We don't need to test all combinations here, that is tested in EditorInfoTest.
486 keyboardOptions =
487 KeyboardOptions(
488 capitalization = KeyboardCapitalization.Characters,
489 keyboardType = KeyboardType.Email,
490 imeAction = ImeAction.Previous
491 )
492 )
493 }
494 requestFocus(Tag)
495
496 inputMethodInterceptor.withEditorInfo {
497 assertThat(imeOptions and EditorInfo.IME_ACTION_PREVIOUS).isNotEqualTo(0)
498 assertThat(inputType and EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS).isNotEqualTo(0)
499 assertThat(inputType and InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS).isNotEqualTo(0)
500 }
501 }
502
503 @Test
504 fun textField_appliesFilter_toInputConnection() {
505 val state = TextFieldState()
506 inputMethodInterceptor.setTextFieldTestContent {
507 BasicTextField(
508 state = state,
509 inputTransformation = RejectAllTextFilter,
510 modifier = Modifier.testTag(Tag)
511 )
512 }
513 requestFocus(Tag)
514
515 inputMethodInterceptor.withInputConnection { commitText("hello") }
516 rule.onNodeWithTag(Tag).assertTextEquals("")
517 }
518
519 @Test
520 fun textField_appliesFilter_toInputConnection_changingComposition() {
521 val state = TextFieldState()
522 inputMethodInterceptor.setTextFieldTestContent {
523 BasicTextField(
524 state = state,
525 inputTransformation = RejectAllTextFilter,
526 modifier = Modifier.testTag(Tag)
527 )
528 }
529 requestFocus(Tag)
530
531 inputMethodInterceptor.withInputConnection { setComposingText("hello", 1) }
532 rule.onNodeWithTag(Tag).assertTextEquals("")
533 assertThat(state.composition).isNull()
534 }
535
536 @Test
537 fun textField_appliesFilter_toSetTextSemanticsAction() {
538 val state = TextFieldState()
539 inputMethodInterceptor.setTextFieldTestContent {
540 BasicTextField(
541 state = state,
542 inputTransformation = RejectAllTextFilter,
543 modifier = Modifier.testTag(Tag)
544 )
545 }
546
547 rule.onNodeWithTag(Tag).performTextReplacement("hello")
548 rule.onNodeWithTag(Tag).assertTextEquals("")
549 }
550
551 @Test
552 fun textField_appliesFilter_toInsertTextSemanticsAction() {
553 val state = TextFieldState()
554 inputMethodInterceptor.setTextFieldTestContent {
555 BasicTextField(
556 state = state,
557 inputTransformation = RejectAllTextFilter,
558 modifier = Modifier.testTag(Tag)
559 )
560 }
561
562 rule.onNodeWithTag(Tag).performTextInput("hello")
563 rule.onNodeWithTag(Tag).assertTextEquals("")
564 }
565
566 @Test
567 fun textField_appliesFilter_toKeyEvents() {
568 val state = TextFieldState()
569 inputMethodInterceptor.setTextFieldTestContent {
570 BasicTextField(
571 state = state,
572 inputTransformation = RejectAllTextFilter,
573 modifier = Modifier.testTag(Tag)
574 )
575 }
576
577 rule.onNodeWithTag(Tag).performKeyInput { pressKey(Key.A) }
578 rule.onNodeWithTag(Tag).assertTextEquals("")
579 }
580
581 @Test
582 fun textField_appliesFilter_toInputConnection_afterChanging() {
583 val state = TextFieldState()
584 var filter by mutableStateOf<InputTransformation?>(null)
585 inputMethodInterceptor.setTextFieldTestContent {
586 BasicTextField(
587 state = state,
588 inputTransformation = filter,
589 modifier = Modifier.testTag(Tag)
590 )
591 }
592 requestFocus(Tag)
593
594 inputMethodInterceptor.withInputConnection { commitText("hello") }
595 rule.onNodeWithTag(Tag).assertTextEquals("hello")
596
597 filter = RejectAllTextFilter
598
599 inputMethodInterceptor.withInputConnection { commitText("world") }
600 rule.onNodeWithTag(Tag).assertTextEquals("hello")
601
602 filter = null
603
604 inputMethodInterceptor.withInputConnection { commitText("world") }
605 rule.onNodeWithTag(Tag).assertTextEquals("helloworld")
606 }
607
608 @Test
609 fun textField_appliesFilter_toSetTextSemanticsAction_afterChanging() {
610 val state = TextFieldState()
611 var filter by mutableStateOf<InputTransformation?>(null)
612 inputMethodInterceptor.setTextFieldTestContent {
613 BasicTextField(
614 state = state,
615 inputTransformation = filter,
616 modifier = Modifier.testTag(Tag)
617 )
618 }
619
620 rule.onNodeWithTag(Tag).performTextInput("hello")
621 rule.onNodeWithTag(Tag).assertTextEquals("hello")
622
623 filter = RejectAllTextFilter
624
625 rule.onNodeWithTag(Tag).performTextReplacement("world")
626 rule.onNodeWithTag(Tag).assertTextEquals("hello")
627
628 filter = null
629
630 rule.onNodeWithTag(Tag).performTextReplacement("world")
631 rule.onNodeWithTag(Tag).assertTextEquals("world")
632 }
633
634 @Test
635 fun textField_appliesFilter_toInsertTextSemanticsAction_afterChanging() {
636 val state = TextFieldState()
637 var filter by mutableStateOf<InputTransformation?>(null)
638 inputMethodInterceptor.setTextFieldTestContent {
639 BasicTextField(
640 state = state,
641 inputTransformation = filter,
642 modifier = Modifier.testTag(Tag)
643 )
644 }
645
646 rule.onNodeWithTag(Tag).performTextInput("hello")
647 rule.onNodeWithTag(Tag).assertTextEquals("hello")
648
649 filter = RejectAllTextFilter
650
651 rule.onNodeWithTag(Tag).performTextInput("world")
652 rule.onNodeWithTag(Tag).assertTextEquals("hello")
653
654 filter = null
655
656 rule.onNodeWithTag(Tag).performTextInput("world")
657 rule.onNodeWithTag(Tag).assertTextEquals("helloworld")
658 }
659
660 @Test
661 fun textField_appliesFilter_toKeyEvents_afterChanging() {
662 val state = TextFieldState()
663 var filter by mutableStateOf<InputTransformation?>(null)
664 inputMethodInterceptor.setTextFieldTestContent {
665 BasicTextField(
666 state = state,
667 inputTransformation = filter,
668 modifier = Modifier.testTag(Tag)
669 )
670 }
671
672 rule.onNodeWithTag(Tag).performTextInput("hello")
673 rule.onNodeWithTag(Tag).assertTextEquals("hello")
674
675 filter = RejectAllTextFilter
676
677 rule.onNodeWithTag(Tag).performKeyInput { pressKey(Key.Spacebar) }
678 rule.onNodeWithTag(Tag).assertTextEquals("hello")
679
680 filter = null
681
682 rule.onNodeWithTag(Tag).performKeyInput { pressKey(Key.Spacebar) }
683 rule.onNodeWithTag(Tag).assertTextEquals("hello ")
684 }
685
686 @Test
687 fun textField_changesAreTracked_whenInputConnectionCommits() {
688 val state = TextFieldState()
689 lateinit var changeList: ChangeList
690 inputMethodInterceptor.setTextFieldTestContent {
691 BasicTextField(
692 state = state,
693 inputTransformation = {
694 if (changes.changeCount > 0) {
695 changeList = changes
696 }
697 },
698 modifier = Modifier.testTag(Tag),
699 )
700 }
701 requestFocus(Tag)
702
703 inputMethodInterceptor.withInputConnection { commitText("hello") }
704
705 rule.runOnIdle {
706 assertThat(changeList.changeCount).isEqualTo(1)
707 assertThat(changeList.getRange(0)).isEqualTo(TextRange(0, 5))
708 assertThat(changeList.getOriginalRange(0)).isEqualTo(TextRange(0, 0))
709 }
710 }
711
712 @Test
713 fun textField_changesAreTracked_whenInputConnectionComposes() {
714 val state = TextFieldState()
715 lateinit var changeList: ChangeList
716 inputMethodInterceptor.setTextFieldTestContent {
717 BasicTextField(
718 state = state,
719 inputTransformation = {
720 if (changes.changeCount > 0) {
721 changeList = changes
722 }
723 },
724 modifier = Modifier.testTag(Tag),
725 )
726 }
727 requestFocus(Tag)
728
729 inputMethodInterceptor.withInputConnection { setComposingText("hello", 1) }
730
731 rule.runOnIdle {
732 assertThat(changeList.changeCount).isEqualTo(1)
733 assertThat(changeList.getRange(0)).isEqualTo(TextRange(0, 5))
734 assertThat(changeList.getOriginalRange(0)).isEqualTo(TextRange(0))
735 }
736 }
737
738 @Test
739 fun textField_changesAreTracked_whenInputConnectionDeletes() {
740 val state = TextFieldState("hello")
741 lateinit var changeList: ChangeList
742 inputMethodInterceptor.setTextFieldTestContent {
743 BasicTextField(
744 state = state,
745 inputTransformation = {
746 if (changes.changeCount > 0) {
747 changeList = changes
748 }
749 },
750 modifier = Modifier.testTag(Tag),
751 )
752 }
753 requestFocus(Tag)
754
755 inputMethodInterceptor.withInputConnection {
756 beginBatchEdit()
757 finishComposingText()
758 setSelection(5, 5)
759 deleteSurroundingText(1, 0)
760 endBatchEdit()
761 }
762
763 rule.runOnIdle {
764 assertThat(changeList.changeCount).isEqualTo(1)
765 assertThat(changeList.getRange(0)).isEqualTo(TextRange(4, 4))
766 assertThat(changeList.getOriginalRange(0)).isEqualTo(TextRange(4, 5))
767 }
768 }
769
770 @Test
771 fun textField_changesAreTracked_whenInputConnectionDeletesViaComposition() {
772 val state = TextFieldState("hello")
773 lateinit var changeList: ChangeList
774 inputMethodInterceptor.setTextFieldTestContent {
775 BasicTextField(
776 state = state,
777 inputTransformation = {
778 if (changes.changeCount > 0) {
779 changeList = changes
780 }
781 },
782 modifier = Modifier.testTag(Tag),
783 )
784 }
785 requestFocus(Tag)
786
787 inputMethodInterceptor.withInputConnection {
788 beginBatchEdit()
789 setComposingRegion(0, 5)
790 setComposingText("h", 1)
791 endBatchEdit()
792 }
793
794 rule.runOnIdle {
795 assertThat(changeList.changeCount).isEqualTo(1)
796 assertThat(changeList.getRange(0)).isEqualTo(TextRange(1, 1))
797 assertThat(changeList.getOriginalRange(0)).isEqualTo(TextRange(1, 5))
798 }
799 }
800
801 @Test
802 fun textField_changesAreTracked_whenKeyEventInserts() {
803 val state = TextFieldState()
804 lateinit var changeList: ChangeList
805 inputMethodInterceptor.setTextFieldTestContent {
806 BasicTextField(
807 state = state,
808 inputTransformation = {
809 if (changes.changeCount > 0) {
810 changeList = changes
811 }
812 },
813 modifier = Modifier.testTag(Tag),
814 )
815 }
816 requestFocus(Tag)
817
818 rule.onNodeWithTag(Tag).performKeyInput { pressKey(Key.A) }
819
820 rule.runOnIdle {
821 assertThat(changeList.changeCount).isEqualTo(1)
822 assertThat(changeList.getRange(0)).isEqualTo(TextRange(0, 1))
823 assertThat(changeList.getOriginalRange(0)).isEqualTo(TextRange(0))
824 }
825 }
826
827 @Test
828 fun textField_changesAreTracked_whenKeyEventDeletes() {
829 val state = TextFieldState("hello")
830 lateinit var changeList: ChangeList
831 inputMethodInterceptor.setTextFieldTestContent {
832 BasicTextField(
833 state = state,
834 inputTransformation = {
835 if (changes.changeCount > 0) {
836 changeList = changes
837 }
838 },
839 modifier = Modifier.testTag(Tag),
840 )
841 }
842
843 rule.onNodeWithTag(Tag).performTextInputSelection(TextRange(5))
844 rule.onNodeWithTag(Tag).performKeyInput { pressKey(Key.Backspace) }
845
846 rule.runOnIdle {
847 assertThat(changeList.changeCount).isEqualTo(1)
848 assertThat(changeList.getRange(0)).isEqualTo(TextRange(4, 4))
849 assertThat(changeList.getOriginalRange(0)).isEqualTo(TextRange(4, 5))
850 }
851 }
852
853 @Test
854 fun textField_changesAreTracked_whenSemanticsActionInserts() {
855 val state = TextFieldState()
856 lateinit var changeList: ChangeList
857 inputMethodInterceptor.setTextFieldTestContent {
858 BasicTextField(
859 state = state,
860 inputTransformation = {
861 if (changes.changeCount > 0) {
862 changeList = changes
863 }
864 },
865 modifier = Modifier.testTag(Tag),
866 )
867 }
868
869 rule.onNodeWithTag(Tag).performTextInput("hello")
870
871 rule.runOnIdle {
872 assertThat(changeList.changeCount).isEqualTo(1)
873 assertThat(changeList.getRange(0)).isEqualTo(TextRange(0, 5))
874 assertThat(changeList.getOriginalRange(0)).isEqualTo(TextRange(0))
875 }
876 }
877
878 @Test
879 fun textField_filterKeyboardOptions_sentToIme() {
880 val filter =
881 KeyboardOptionsFilter(
882 KeyboardOptions(keyboardType = KeyboardType.Email, imeAction = ImeAction.Previous)
883 )
884 inputMethodInterceptor.setTextFieldTestContent {
885 BasicTextField(
886 state = rememberTextFieldState(),
887 modifier = Modifier.testTag(Tag),
888 inputTransformation = filter,
889 )
890 }
891 requestFocus(Tag)
892
893 inputMethodInterceptor.withEditorInfo {
894 assertThat(imeOptions and EditorInfo.IME_ACTION_PREVIOUS).isNotEqualTo(0)
895 assertThat(inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS).isNotEqualTo(0)
896 }
897 }
898
899 @Test
900 fun textField_filterKeyboardOptions_mergedWithParams() {
901 val filter = KeyboardOptionsFilter(KeyboardOptions(imeAction = ImeAction.Previous))
902 inputMethodInterceptor.setTextFieldTestContent {
903 BasicTextField(
904 state = rememberTextFieldState(),
905 modifier = Modifier.testTag(Tag),
906 inputTransformation = filter,
907 keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
908 )
909 }
910 requestFocus(Tag)
911
912 inputMethodInterceptor.withEditorInfo {
913 assertThat(imeOptions and EditorInfo.IME_ACTION_PREVIOUS).isNotEqualTo(0)
914 assertThat(inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS).isNotEqualTo(0)
915 }
916 }
917
918 @Test
919 fun textField_filterKeyboardOptions_overriddenByParams() {
920 val filter = KeyboardOptionsFilter(KeyboardOptions(imeAction = ImeAction.Previous))
921 inputMethodInterceptor.setTextFieldTestContent {
922 BasicTextField(
923 state = rememberTextFieldState(),
924 modifier = Modifier.testTag(Tag),
925 inputTransformation = filter,
926 keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
927 )
928 }
929 requestFocus(Tag)
930
931 inputMethodInterceptor.withEditorInfo {
932 assertThat(imeOptions and EditorInfo.IME_ACTION_SEARCH).isNotEqualTo(0)
933 }
934 }
935
936 @Test
937 fun textField_filterKeyboardOptions_applyWhenFilterChanged() {
938 var filter by
939 mutableStateOf(
940 KeyboardOptionsFilter(
941 KeyboardOptions(
942 keyboardType = KeyboardType.Email,
943 imeAction = ImeAction.Previous
944 )
945 )
946 )
947 inputMethodInterceptor.setTextFieldTestContent {
948 BasicTextField(
949 state = rememberTextFieldState(),
950 modifier = Modifier.testTag(Tag),
951 inputTransformation = filter,
952 )
953 }
954 requestFocus(Tag)
955
956 inputMethodInterceptor.withEditorInfo {
957 assertThat(imeOptions and EditorInfo.IME_ACTION_PREVIOUS).isNotEqualTo(0)
958 assertThat(inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS).isNotEqualTo(0)
959 }
960
961 filter =
962 KeyboardOptionsFilter(
963 KeyboardOptions(keyboardType = KeyboardType.Decimal, imeAction = ImeAction.Search)
964 )
965
966 inputMethodInterceptor.withEditorInfo {
967 assertThat(imeOptions and EditorInfo.IME_ACTION_SEARCH).isNotEqualTo(0)
968 assertThat(inputType and InputType.TYPE_NUMBER_FLAG_DECIMAL).isNotEqualTo(0)
969 }
970 }
971
972 @Test
973 fun textField_filterKeyboardOptions_applyWhenKeyboardOptionsChanged() {
974 var keyboardOptionsState by
975 mutableStateOf(
976 KeyboardOptions(keyboardType = KeyboardType.Email, imeAction = ImeAction.Previous)
977 )
978 val filter =
979 object : InputTransformation {
980 override val keyboardOptions: KeyboardOptions
981 get() = keyboardOptionsState
982
983 override fun TextFieldBuffer.transformInput() = Unit
984 }
985
986 inputMethodInterceptor.setTextFieldTestContent {
987 BasicTextField(
988 state = rememberTextFieldState(),
989 modifier = Modifier.testTag(Tag),
990 inputTransformation = filter,
991 )
992 }
993 requestFocus(Tag)
994
995 inputMethodInterceptor.withEditorInfo {
996 assertThat(imeOptions and EditorInfo.IME_ACTION_PREVIOUS).isNotEqualTo(0)
997 assertThat(inputType and InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS).isNotEqualTo(0)
998 }
999
1000 keyboardOptionsState =
1001 KeyboardOptions(keyboardType = KeyboardType.Decimal, imeAction = ImeAction.Search)
1002
1003 inputMethodInterceptor.withEditorInfo {
1004 assertThat(imeOptions and EditorInfo.IME_ACTION_SEARCH).isNotEqualTo(0)
1005 assertThat(inputType and InputType.TYPE_NUMBER_FLAG_DECIMAL).isNotEqualTo(0)
1006 }
1007 }
1008
1009 @SdkSuppress(minSdkVersion = 23)
1010 @Test
1011 fun textField_showsKeyboardAgainWhenTapped_ifFocused() {
1012 val testKeyboardController = TestSoftwareKeyboardController(rule)
1013 inputMethodInterceptor.setTextFieldTestContent {
1014 CompositionLocalProvider(
1015 LocalSoftwareKeyboardController provides testKeyboardController
1016 ) {
1017 BasicTextField(state = rememberTextFieldState(), modifier = Modifier.testTag(Tag))
1018 }
1019 }
1020 // Focusing the field will show the keyboard without using the SoftwareKeyboardController.
1021 rule.onNodeWithTag(Tag).requestFocus()
1022 testKeyboardController.hide()
1023
1024 // This will go through the SoftwareKeyboardController to show the keyboard, since a session
1025 // is already active.
1026 rule.onNodeWithTag(Tag).performClick()
1027
1028 testKeyboardController.assertShown()
1029 }
1030
1031 @Test
1032 fun swipingThroughTextField_doesNotGainFocus() {
1033 inputMethodInterceptor.setTextFieldTestContent {
1034 BasicTextField(state = rememberTextFieldState(), modifier = Modifier.testTag(Tag))
1035 }
1036
1037 rule.onNodeWithTag(Tag).performTouchInput {
1038 // swipe through
1039 swipeRight(endX = right + 200, durationMillis = 100)
1040 }
1041 rule.onNodeWithTag(Tag).assertIsNotFocused()
1042 }
1043
1044 @Test
1045 fun swipingTextFieldInScrollableContainer_doesNotGainFocus() {
1046 val scrollState = ScrollState(0)
1047 inputMethodInterceptor.setTextFieldTestContent {
1048 Column(Modifier.height(100.dp).verticalScroll(scrollState)) {
1049 BasicTextField(state = rememberTextFieldState(), modifier = Modifier.testTag(Tag))
1050 Box(Modifier.height(200.dp))
1051 }
1052 }
1053
1054 rule.onNodeWithTag(Tag).performTouchInput { swipeUp() }
1055 rule.onNodeWithTag(Tag).assertIsNotFocused()
1056 assertThat(scrollState.value).isNotEqualTo(0)
1057 }
1058
1059 @Test
1060 fun densityChanges_causesRelayout() {
1061 val state = TextFieldState("Hello")
1062 var density by mutableStateOf(Density(1f))
1063 val fontSize = 20.sp
1064 inputMethodInterceptor.setTextFieldTestContent {
1065 CompositionLocalProvider(LocalDensity provides density) {
1066 BasicTextField(
1067 state = state,
1068 textStyle = TextStyle(fontFamily = TEST_FONT_FAMILY, fontSize = fontSize),
1069 modifier = Modifier.testTag(Tag)
1070 )
1071 }
1072 }
1073
1074 val firstSize = rule.onNodeWithTag(Tag).fetchTextLayoutResult().size
1075
1076 density = Density(2f)
1077
1078 val secondSize = rule.onNodeWithTag(Tag).fetchTextLayoutResult().size
1079
1080 assertThat(secondSize.width).isEqualTo(firstSize.width * 2)
1081 assertThat(secondSize.height).isEqualTo(firstSize.height * 2)
1082 }
1083
1084 // Regression test for b/311834126
1085 @Test
1086 fun whenPastingTextThatIncreasesEndOffset_noCrashAndCursorAtEndOfPastedText() = runTest {
1087 val longText = "Text".repeat(4)
1088 val shortText = "Text".repeat(2)
1089
1090 lateinit var tfs: TextFieldState
1091 val clipboard =
1092 object : Clipboard {
1093 var contents: AnnotatedString? = null
1094
1095 override suspend fun getClipEntry(): ClipEntry? {
1096 return contents?.toClipEntry()
1097 }
1098
1099 override suspend fun setClipEntry(clipEntry: ClipEntry?) {
1100 contents = clipEntry?.readAnnotatedString()
1101 }
1102
1103 override val nativeClipboard: NativeClipboard
1104 get() = error("FakeClipboard doesn't have a backing NativeClipboard")
1105 }
1106 inputMethodInterceptor.setTextFieldTestContent {
1107 tfs = rememberTextFieldState(shortText)
1108 CompositionLocalProvider(LocalClipboard provides clipboard) {
1109 BasicTextField(
1110 state = tfs,
1111 modifier = Modifier.testTag(Tag),
1112 )
1113 }
1114 }
1115 clipboard.setClipEntry(AnnotatedString(longText).toClipEntry())
1116 rule.waitForIdle()
1117
1118 val node = rule.onNodeWithTag(Tag)
1119 node.performTouchInput { longClick(center) }
1120 rule.waitForIdle()
1121
1122 node.performSemanticsAction(SemanticsActions.PasteText) { it() }
1123 rule.waitForIdle()
1124
1125 assertThat(tfs.text.toString()).isEqualTo(longText)
1126 assertThat(tfs.selection).isEqualTo(TextRange(longText.length))
1127 }
1128
1129 @Test
1130 fun selectAll_contextMenuAction_informsImeOfSelectionChange() {
1131 immRule.setFactory { imm }
1132 val state = TextFieldState("Hello")
1133 inputMethodInterceptor.setTextFieldTestContent {
1134 BasicTextField(state = state, modifier = Modifier.testTag(Tag))
1135 }
1136
1137 requestFocus(Tag)
1138
1139 inputMethodInterceptor.withInputConnection { performContextMenuAction(R.id.selectAll) }
1140
1141 rule.runOnIdle {
1142 assertThat(state.selection).isEqualTo(TextRange(0, 5))
1143 assertThat(imm.expectCall("updateSelection(0, 5, -1, -1)"))
1144 }
1145 }
1146
1147 @Test
1148 fun cut_contextMenuAction_cutsIntoClipboard() = runTest {
1149 val clipboard = FakeClipboard("World")
1150 val state = TextFieldState("Hello", initialSelection = TextRange(0, 2))
1151 inputMethodInterceptor.setTextFieldTestContent {
1152 CompositionLocalProvider(LocalClipboard provides clipboard) {
1153 BasicTextField(state = state, modifier = Modifier.testTag(Tag))
1154 }
1155 }
1156
1157 requestFocus(Tag)
1158
1159 inputMethodInterceptor.withInputConnection { performContextMenuAction(R.id.cut) }
1160
1161 rule.waitForIdle()
1162 assertThat(clipboard.getClipEntry()?.readText()).isEqualTo("He")
1163 assertThat(state.text.toString()).isEqualTo("llo")
1164 }
1165
1166 @Test
1167 fun copy_contextMenuAction_copiesIntoClipboard() = runTest {
1168 val clipboard = FakeClipboard("World")
1169 val state = TextFieldState("Hello", initialSelection = TextRange(0, 2))
1170 inputMethodInterceptor.setTextFieldTestContent {
1171 CompositionLocalProvider(LocalClipboard provides clipboard) {
1172 BasicTextField(state = state, modifier = Modifier.testTag(Tag))
1173 }
1174 }
1175
1176 requestFocus(Tag)
1177
1178 inputMethodInterceptor.withInputConnection { performContextMenuAction(R.id.copy) }
1179
1180 rule.waitForIdle()
1181 assertThat(clipboard.getClipEntry()?.readText()).isEqualTo("He")
1182 }
1183
1184 @Test
1185 fun paste_contextMenuAction_pastesFromClipboard() {
1186 val clipboard = FakeClipboard("World")
1187 val state = TextFieldState("Hello", initialSelection = TextRange(0, 4))
1188 inputMethodInterceptor.setTextFieldTestContent {
1189 CompositionLocalProvider(LocalClipboard provides clipboard) {
1190 BasicTextField(state = state, modifier = Modifier.testTag(Tag))
1191 }
1192 }
1193
1194 requestFocus(Tag)
1195
1196 inputMethodInterceptor.withInputConnection { performContextMenuAction(R.id.paste) }
1197
1198 rule.runOnIdle {
1199 assertThat(state.text.toString()).isEqualTo("Worldo")
1200 assertThat(state.selection).isEqualTo(TextRange(5))
1201 }
1202 }
1203
1204 @Test
1205 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
1206 fun textField_textAlignCenter_defaultWidth() {
1207 val fontSize = 50
1208 val density = Density(1f, 1f)
1209 val textStyle =
1210 TextStyle(
1211 textAlign = TextAlign.Center,
1212 color = Color.Black,
1213 fontFamily = TEST_FONT_FAMILY,
1214 fontSize = fontSize.sp
1215 )
1216 rule.setContent {
1217 CompositionLocalProvider(LocalDensity provides density) {
1218 BasicTextField(
1219 modifier = Modifier.testTag(Tag),
1220 state = rememberTextFieldState("A"),
1221 textStyle = textStyle,
1222 lineLimits = TextFieldLineLimits.SingleLine
1223 )
1224 }
1225 }
1226
1227 rule.waitForIdle()
1228 rule.onNodeWithTag(Tag).captureToImage().assertHorizontallySymmetrical(fontSize)
1229 }
1230
1231 @Test
1232 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
1233 fun textField_textAlignCenter_widthSmallerThanDefaultWidth() {
1234 val fontSize = 50
1235 val density = Density(1f, 1f)
1236 val textStyle =
1237 TextStyle(
1238 textAlign = TextAlign.Center,
1239 color = Color.Black,
1240 fontFamily = TEST_FONT_FAMILY,
1241 fontSize = fontSize.sp
1242 )
1243 rule.setContent {
1244 val fontFamilyResolver = LocalFontFamilyResolver.current
1245 val defaultWidth =
1246 computeSizeForDefaultText(
1247 style = textStyle,
1248 density = density,
1249 fontFamilyResolver = fontFamilyResolver,
1250 maxLines = 1
1251 )
1252 .width
1253
1254 CompositionLocalProvider(LocalDensity provides density) {
1255 BasicTextField(
1256 modifier = Modifier.testTag(Tag).width(defaultWidth.dp / 2),
1257 state = rememberTextFieldState("A"),
1258 textStyle = textStyle,
1259 lineLimits = TextFieldLineLimits.SingleLine
1260 )
1261 }
1262 }
1263
1264 rule.waitForIdle()
1265 rule.onNodeWithTag(Tag).captureToImage().assertHorizontallySymmetrical(fontSize)
1266 }
1267
1268 @Test
1269 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
1270 fun textField_textAlignCenter_widthLargerThanDefaultWidth() {
1271 val fontSize = 50
1272 val density = Density(1f, 1f)
1273 val textStyle =
1274 TextStyle(
1275 textAlign = TextAlign.Center,
1276 color = Color.Black,
1277 fontFamily = TEST_FONT_FAMILY,
1278 fontSize = fontSize.sp
1279 )
1280 rule.setContent {
1281 val fontFamilyResolver = LocalFontFamilyResolver.current
1282 val defaultWidth =
1283 computeSizeForDefaultText(
1284 style = textStyle,
1285 density = density,
1286 fontFamilyResolver = fontFamilyResolver,
1287 maxLines = 1
1288 )
1289 .width
1290
1291 CompositionLocalProvider(LocalDensity provides density) {
1292 BasicTextField(
1293 modifier = Modifier.testTag(Tag).width(defaultWidth.dp * 2),
1294 state = rememberTextFieldState("A"),
1295 textStyle = textStyle,
1296 lineLimits = TextFieldLineLimits.SingleLine
1297 )
1298 }
1299 }
1300
1301 rule.waitForIdle()
1302 rule.onNodeWithTag(Tag).captureToImage().assertHorizontallySymmetrical(fontSize)
1303 }
1304
1305 @Test
1306 fun textField_state_invokesAutofill() {
1307 val mockLambda: () -> Unit = mock()
1308 var density by mutableStateOf(Density(1f))
1309
1310 val manager =
1311 TextFieldSelectionState(
1312 // other parameters not necessary to test autofill invocation
1313 textFieldState =
1314 TransformedTextFieldState(
1315 textFieldState = TextFieldState(),
1316 inputTransformation = null,
1317 codepointTransformation = null,
1318 outputTransformation = null
1319 ),
1320 textLayoutState = TextLayoutState(),
1321 density = density,
1322 enabled = true,
1323 readOnly = false,
1324 isFocused = false,
1325 isPassword = false
1326 )
1327 .apply { requestAutofillAction = mockLambda }
1328
1329 manager.autofill()
1330 verify(mockLambda, times(1)).invoke()
1331 }
1332
1333 @Test
1334 fun changingInputTransformation_doesNotRestartInput() {
1335 var inputTransformation by mutableStateOf(InputTransformation.maxLength(10))
1336 inputMethodInterceptor.setTextFieldTestContent {
1337 val state = remember { TextFieldState() }
1338 BasicTextField(
1339 state = state,
1340 modifier = Modifier.fillMaxSize().testTag(Tag),
1341 inputTransformation = inputTransformation
1342 )
1343 }
1344
1345 requestFocus(Tag)
1346 inputMethodInterceptor.assertSessionActive()
1347 inputMethodInterceptor.assertThatSessionCount().isEqualTo(1)
1348
1349 inputTransformation = InputTransformation.maxLength(15)
1350
1351 inputMethodInterceptor.assertSessionActive()
1352 inputMethodInterceptor.assertThatSessionCount().isEqualTo(1)
1353 }
1354
1355 @Test
1356 fun changingInputTransformation_restartsInput_ifKeyboardOptionsChange() {
1357 var inputTransformation by mutableStateOf<InputTransformation?>(null)
1358 inputMethodInterceptor.setTextFieldTestContent {
1359 val state = remember { TextFieldState() }
1360 BasicTextField(
1361 state = state,
1362 modifier = Modifier.fillMaxSize().testTag(Tag),
1363 inputTransformation = inputTransformation
1364 )
1365 }
1366
1367 requestFocus(Tag)
1368 inputMethodInterceptor.assertSessionActive()
1369 inputMethodInterceptor.assertThatSessionCount().isEqualTo(1)
1370
1371 inputTransformation = InputTransformation.allCaps(Locale.current)
1372
1373 inputMethodInterceptor.assertSessionActive()
1374 inputMethodInterceptor.assertThatSessionCount().isEqualTo(2)
1375 }
1376
1377 @Test
1378 fun composingRegion_addsUnderlineSpanToLayout() {
1379 val state = TextFieldState("Hello, World")
1380 var textLayoutProvider: (() -> TextLayoutResult?)? by mutableStateOf(null)
1381
1382 inputMethodInterceptor.setTextFieldTestContent {
1383 BasicTextField(
1384 state = state,
1385 modifier = Modifier.fillMaxSize().testTag(Tag),
1386 onTextLayout = { textLayoutProvider = it }
1387 )
1388 }
1389
1390 requestFocus(Tag)
1391 inputMethodInterceptor.withInputConnection { setComposingRegion(0, 5) }
1392 rule.runOnIdle {
1393 val currentTextLayout = textLayoutProvider?.invoke()
1394 assertThat(currentTextLayout).isNotNull()
1395
1396 val expectedSpan =
1397 AnnotatedString.Range(
1398 item = SpanStyle(textDecoration = TextDecoration.Underline),
1399 start = 0,
1400 end = 5
1401 )
1402 assertThat(currentTextLayout!!.multiParagraph.intrinsics.annotatedString.spanStyles)
1403 .contains(expectedSpan)
1404 }
1405 }
1406
1407 @Test
1408 fun composingRegion_changesInvalidateLayout() {
1409 val state = TextFieldState("Hello, World")
1410 var textLayoutProvider: (() -> TextLayoutResult?)? by mutableStateOf(null)
1411 state.editAsUser(inputTransformation = null) { setComposition(0, 5) }
1412
1413 inputMethodInterceptor.setTextFieldTestContent {
1414 BasicTextField(
1415 state = state,
1416 modifier = Modifier.fillMaxSize().testTag(Tag),
1417 onTextLayout = { textLayoutProvider = it }
1418 )
1419 }
1420
1421 requestFocus(Tag)
1422
1423 // assert initial composing region
1424 rule.runOnIdle {
1425 val initialTextLayout = textLayoutProvider?.invoke()
1426 assertThat(initialTextLayout).isNotNull()
1427
1428 val expectedSpan =
1429 AnnotatedString.Range(
1430 item = SpanStyle(textDecoration = TextDecoration.Underline),
1431 start = 0,
1432 end = 5
1433 )
1434 assertThat(initialTextLayout!!.multiParagraph.intrinsics.annotatedString.spanStyles)
1435 .contains(expectedSpan)
1436 }
1437
1438 // change composing region
1439 inputMethodInterceptor.withInputConnection { setComposingRegion(7, 12) }
1440
1441 // assert the changed region
1442 rule.runOnIdle {
1443 val currentTextLayout = textLayoutProvider?.invoke()
1444 assertThat(currentTextLayout).isNotNull()
1445
1446 val expectedSpan =
1447 AnnotatedString.Range(
1448 item = SpanStyle(textDecoration = TextDecoration.Underline),
1449 start = 7,
1450 end = 12
1451 )
1452 assertThat(currentTextLayout!!.multiParagraph.intrinsics.annotatedString.spanStyles)
1453 .contains(expectedSpan)
1454 }
1455 }
1456
1457 @Test
1458 fun phoneKeyboardType_RtlLocaleLtrDigits_resolvesToLtrTextDirection() {
1459 val state = TextFieldState()
1460 var textLayoutProvider: (() -> TextLayoutResult?)? by mutableStateOf(null)
1461
1462 inputMethodInterceptor.setTextFieldTestContent {
1463 BasicTextField(
1464 state = state,
1465 modifier = Modifier.fillMaxSize().testTag(Tag),
1466 textStyle = TextStyle(localeList = LocaleList("ar")),
1467 keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
1468 onTextLayout = { textLayoutProvider = it }
1469 )
1470 }
1471
1472 rule.runOnIdle {
1473 // this would normally have been Unspecified.
1474 assertThat(textLayoutProvider?.invoke()?.layoutInput?.style?.textDirection)
1475 .isEqualTo(TextDirection.Ltr)
1476 }
1477 }
1478
1479 @SdkSuppress(minSdkVersion = 31) // Adlam digits were added in API 31
1480 @Test
1481 fun phoneKeyboardType_RtlLocaleRtlDigits_resolvesToRtlTextDirection() {
1482 val state = TextFieldState()
1483 var textLayoutProvider: (() -> TextLayoutResult?)? by mutableStateOf(null)
1484
1485 inputMethodInterceptor.setTextFieldTestContent {
1486 BasicTextField(
1487 state = state,
1488 modifier = Modifier.fillMaxSize().testTag(Tag),
1489 textStyle = TextStyle(localeList = LocaleList("ff-Adlm-BF")),
1490 keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
1491 onTextLayout = { textLayoutProvider = it }
1492 )
1493 }
1494
1495 rule.runOnIdle {
1496 // this would normally have been Unspecified.
1497 assertThat(textLayoutProvider?.invoke()?.layoutInput?.style?.textDirection)
1498 .isEqualTo(TextDirection.Rtl)
1499 }
1500 }
1501
1502 @Test
1503 fun longText_doesNotCrash() {
1504 var textLayoutProvider: (() -> TextLayoutResult?)? = null
1505 inputMethodInterceptor.setTextFieldTestContent {
1506 BasicTextField(
1507 rememberTextFieldState("A".repeat(100_000)),
1508 onTextLayout = { textLayoutProvider = it }
1509 )
1510 }
1511
1512 rule.runOnIdle {
1513 assertThat(textLayoutProvider?.invoke()?.layoutInput?.text?.length).isEqualTo(100_000)
1514 }
1515 }
1516
1517 @Test
1518 fun whenElementFocusLost_compositionIsCleared() {
1519 lateinit var focusManager: FocusManager
1520 val focusRequester = FocusRequester()
1521 val state = TextFieldState()
1522 inputMethodInterceptor.setTextFieldTestContent {
1523 focusManager = LocalFocusManager.current
1524 BasicTextField(state, Modifier.focusRequester(focusRequester))
1525 }
1526
1527 rule.runOnIdle { focusRequester.requestFocus() }
1528
1529 inputMethodInterceptor.withInputConnection { setComposingText("Hello", 1) }
1530
1531 rule.runOnIdle {
1532 assertThat(state.text.toString()).isEqualTo("Hello")
1533 assertThat(state.composition).isEqualTo(TextRange(0, 5))
1534 }
1535
1536 // setTextFieldTestContent puts a focusable box before the content that's set here
1537 focusManager.moveFocus(FocusDirection.Previous)
1538
1539 rule.runOnIdle {
1540 assertThat(state.text.toString()).isEqualTo("Hello")
1541 assertThat(state.composition).isNull()
1542 }
1543 }
1544
1545 @Test
1546 fun whenWindowFocusLost_compositionRemains() {
1547 val focusRequester = FocusRequester()
1548 val state = TextFieldState()
1549 var windowInfo: WindowInfo by
1550 mutableStateOf(
1551 object : WindowInfo {
1552 override val isWindowFocused = true
1553 }
1554 )
1555 inputMethodInterceptor.setContent {
1556 CompositionLocalProvider(LocalWindowInfo provides windowInfo) {
1557 BasicTextField(state, Modifier.focusRequester(focusRequester))
1558 }
1559 }
1560
1561 rule.runOnIdle { focusRequester.requestFocus() }
1562
1563 inputMethodInterceptor.withInputConnection { setComposingText("Hello", 1) }
1564
1565 rule.runOnIdle {
1566 assertThat(state.text.toString()).isEqualTo("Hello")
1567 assertThat(state.composition).isEqualTo(TextRange(0, 5))
1568 }
1569
1570 windowInfo =
1571 object : WindowInfo {
1572 override val isWindowFocused = false
1573 }
1574
1575 rule.runOnIdle {
1576 assertThat(state.text.toString()).isEqualTo("Hello")
1577 assertThat(state.composition).isEqualTo(TextRange(0, 5))
1578 }
1579 }
1580
1581 @Test
1582 fun whenWindowFocusGained_unfocusedTextFieldStateIsNotRecomposed() {
1583 val state = TextFieldState("Hello")
1584 var isWindowFocused by mutableStateOf(false)
1585 var windowInfo =
1586 object : WindowInfo {
1587 override val isWindowFocused: Boolean
1588 get() = isWindowFocused
1589 }
1590 var decoratorCallCount = 0
1591 val decorator = TextFieldDecorator { innerTextField ->
1592 decoratorCallCount++
1593 innerTextField()
1594 }
1595 rule.setContent {
1596 CompositionLocalProvider(LocalWindowInfo provides windowInfo) {
1597 BasicTextField(
1598 state = state,
1599 decorator = decorator,
1600 )
1601 }
1602 }
1603
1604 val initialDecoratorCallCount = rule.runOnIdle { decoratorCallCount }
1605 isWindowFocused = true
1606
1607 rule.runOnIdle { assertThat(decoratorCallCount).isEqualTo(initialDecoratorCallCount) }
1608 }
1609
1610 // regression test for b/355900176#comment2
1611 @OptIn(ExperimentalComposeUiApi::class)
1612 @Test
1613 fun existingInputSession_doesNotSpillOver_toAnotherTextField() {
1614 inputMethodInterceptor.setContent {
1615 Column {
1616 BasicTextField(rememberTextFieldState(), modifier = Modifier.testTag("btf1"))
1617 InterceptPlatformTextInput({ _, _ -> awaitCancellation() }) {
1618 BasicTextField(rememberTextFieldState(), modifier = Modifier.testTag("btf2"))
1619 }
1620 }
1621 }
1622
1623 rule.onNodeWithTag("btf1").requestFocus()
1624 inputMethodInterceptor.assertSessionActive()
1625
1626 rule.onNodeWithTag("btf2").requestFocus()
1627 inputMethodInterceptor.assertNoSessionActive()
1628
1629 imm.resetCalls()
1630
1631 // successive touches should not start the input
1632 rule.onNodeWithTag("btf2").performClick()
1633 inputMethodInterceptor.assertNoSessionActive()
1634 // InputMethodManager should not have received a showSoftInput() call
1635 rule.runOnIdle { imm.expectNoMoreCalls() }
1636 }
1637
1638 @Test
1639 fun outputTransformation_doesNotLoseComposingAnnotations() {
1640 val textFieldState = TextFieldState()
1641 var textLayoutProvider: (() -> TextLayoutResult?)? = null
1642 inputMethodInterceptor.setTextFieldTestContent {
1643 BasicTextField(
1644 textFieldState,
1645 onTextLayout = { textLayoutProvider = it },
1646 outputTransformation = { append(" world") }
1647 )
1648 }
1649
1650 rule.onNode(hasPerformImeAction()).requestFocus()
1651
1652 val spanned =
1653 SpannableStringBuilder()
1654 .append("Hello", BackgroundColorSpan(android.graphics.Color.RED), 0)
1655 .toSpanned()
1656
1657 inputMethodInterceptor.withInputConnection { setComposingText(spanned, 1) }
1658
1659 rule.runOnIdle {
1660 val textLayoutResult = textLayoutProvider?.invoke()
1661 assertNotNull(textLayoutResult)
1662 val annotatedString = textLayoutResult.layoutInput.text
1663 val spanStyles = annotatedString.spanStyles
1664 assertThat(annotatedString.toString()).isEqualTo("Hello world")
1665 assertThat(spanStyles.size).isEqualTo(1)
1666 assertThat(spanStyles.first().start).isEqualTo(0)
1667 assertThat(spanStyles.first().end).isEqualTo(5)
1668 assertThat(spanStyles.first().item.background).isEqualTo(Color.Red)
1669 }
1670 }
1671
1672 private fun requestFocus(tag: String) = rule.onNodeWithTag(tag).requestFocus()
1673
1674 private fun assertTextSelection(expected: TextRange) {
1675 val selection =
1676 rule.onNodeWithTag(Tag).fetchSemanticsNode().config.getOrNull(TextSelectionRange)
1677 assertThat(selection).isEqualTo(expected)
1678 }
1679
1680 private fun InputConnection.commitText(text: String) {
1681 beginBatchEdit()
1682 finishComposingText()
1683 commitText(text, 1)
1684 endBatchEdit()
1685 }
1686
1687 private object RejectAllTextFilter : InputTransformation {
1688 override fun TextFieldBuffer.transformInput() {
1689 revertAllChanges()
1690 }
1691 }
1692
1693 private class KeyboardOptionsFilter(override val keyboardOptions: KeyboardOptions) :
1694 InputTransformation {
1695 override fun TextFieldBuffer.transformInput() {
1696 // Noop
1697 }
1698 }
1699 }
1700
1701 /**
1702 * Checks whether the given image is horizontally symmetrical where a region that has the width of
1703 * [excludedWidth] around the center is excluded.
1704 */
assertHorizontallySymmetricalnull1705 private fun ImageBitmap.assertHorizontallySymmetrical(excludedWidth: Int) {
1706 val pixel = toPixelMap()
1707 for (y in 0 until height) {
1708 for (x in 0 until (width - excludedWidth) / 2) {
1709 val leftPixel = pixel[x, y]
1710 pixel.assertPixelColor(leftPixel, width - 1 - x, y)
1711 }
1712 }
1713 }
1714