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