Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2020 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 | |
Louis Pullen-Freilich | a11fd94 | 2020-07-24 19:31:19 +0100 | [diff] [blame] | 17 | package androidx.compose.ui.focus |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 18 | |
Andrey Kulikov | 3832037 | 2020-05-07 19:55:05 +0100 | [diff] [blame] | 19 | import android.view.View |
Mihai Popa | 0189e8a | 2020-09-16 13:23:09 +0100 | [diff] [blame] | 20 | import androidx.compose.foundation.layout.Box |
Louis Pullen-Freilich | a03fd6c | 2020-07-24 23:26:29 +0100 | [diff] [blame] | 21 | import androidx.compose.ui.Modifier |
Ralston Da Silva | b497e22 | 2020-08-12 13:35:42 -0700 | [diff] [blame] | 22 | import androidx.compose.ui.focus.FocusState.Active |
| 23 | import androidx.compose.ui.focus.FocusState.Inactive |
Louis Pullen-Freilich | 209df68 | 2020-11-11 00:13:18 +0000 | [diff] [blame] | 24 | import androidx.compose.ui.platform.AmbientView |
Ralston Da Silva | 262f1d6 | 2021-01-06 03:09:02 -0800 | [diff] [blame] | 25 | import androidx.compose.ui.platform.testTag |
Filip Pavlis | ce148943 | 2020-10-29 12:18:06 +0000 | [diff] [blame] | 26 | import androidx.compose.ui.test.junit4.createComposeRule |
Ralston Da Silva | 262f1d6 | 2021-01-06 03:09:02 -0800 | [diff] [blame] | 27 | import androidx.compose.ui.test.onNodeWithTag |
| 28 | import androidx.compose.ui.test.performClick |
Jelle Fresen | 53dd7b7 | 2020-09-25 10:02:27 +0100 | [diff] [blame] | 29 | import androidx.test.ext.junit.runners.AndroidJUnit4 |
| 30 | import androidx.test.filters.MediumTest |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 31 | import com.google.common.truth.Truth.assertThat |
| 32 | import org.junit.Ignore |
| 33 | import org.junit.Rule |
| 34 | import org.junit.Test |
| 35 | import org.junit.runner.RunWith |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 36 | |
Jelle Fresen | 53dd7b7 | 2020-09-25 10:02:27 +0100 | [diff] [blame] | 37 | @MediumTest |
Jelle Fresen | 17628d7 | 2020-09-24 16:23:51 +0100 | [diff] [blame] | 38 | @RunWith(AndroidJUnit4::class) |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 39 | class OwnerFocusTest { |
| 40 | @get:Rule |
Filip Pavlis | 375534f | 2020-09-03 16:59:33 +0100 | [diff] [blame] | 41 | val rule = createComposeRule() |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 42 | |
| 43 | @Test |
| 44 | fun requestFocus_bringsViewInFocus() { |
| 45 | // Arrange. |
Andrey Kulikov | 3832037 | 2020-05-07 19:55:05 +0100 | [diff] [blame] | 46 | lateinit var ownerView: View |
Ralston Da Silva | 71a5eaf | 2020-12-15 18:01:50 -0800 | [diff] [blame] | 47 | val focusRequester = FocusRequester() |
Filip Pavlis | 375534f | 2020-09-03 16:59:33 +0100 | [diff] [blame] | 48 | rule.setFocusableContent { |
Ralston Da Silva | bdaaf17 | 2021-01-11 14:23:39 -0800 | [diff] [blame] | 49 | ownerView = AmbientView.current |
Ralston Da Silva | ad70d26 | 2020-07-24 12:40:16 -0700 | [diff] [blame] | 50 | Box( |
| 51 | modifier = Modifier |
Ralston Da Silva | 71a5eaf | 2020-12-15 18:01:50 -0800 | [diff] [blame] | 52 | .focusRequester(focusRequester) |
Ralston Da Silva | 24cb7a0 | 2020-12-08 17:08:54 -0800 | [diff] [blame] | 53 | .focusModifier() |
Ralston Da Silva | ad70d26 | 2020-07-24 12:40:16 -0700 | [diff] [blame] | 54 | ) |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 55 | } |
| 56 | |
| 57 | // Act. |
Filip Pavlis | 375534f | 2020-09-03 16:59:33 +0100 | [diff] [blame] | 58 | rule.runOnIdle { |
Ralston Da Silva | 71a5eaf | 2020-12-15 18:01:50 -0800 | [diff] [blame] | 59 | focusRequester.requestFocus() |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 60 | } |
| 61 | |
| 62 | // Assert. |
Filip Pavlis | 375534f | 2020-09-03 16:59:33 +0100 | [diff] [blame] | 63 | rule.runOnIdle { |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 64 | assertThat(ownerView.isFocused).isTrue() |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | @Ignore("Enable this test after the owner propagates focus to the hierarchy (b/152535715)") |
| 69 | @Test |
| 70 | fun whenOwnerGainsFocus_focusModifiersAreUpdated() { |
| 71 | // Arrange. |
Andrey Kulikov | 3832037 | 2020-05-07 19:55:05 +0100 | [diff] [blame] | 72 | lateinit var ownerView: View |
Ralston Da Silva | ad70d26 | 2020-07-24 12:40:16 -0700 | [diff] [blame] | 73 | var focusState = Inactive |
Ralston Da Silva | 71a5eaf | 2020-12-15 18:01:50 -0800 | [diff] [blame] | 74 | val focusRequester = FocusRequester() |
Filip Pavlis | 375534f | 2020-09-03 16:59:33 +0100 | [diff] [blame] | 75 | rule.setFocusableContent { |
Ralston Da Silva | bdaaf17 | 2021-01-11 14:23:39 -0800 | [diff] [blame] | 76 | ownerView = AmbientView.current |
Ralston Da Silva | ad70d26 | 2020-07-24 12:40:16 -0700 | [diff] [blame] | 77 | Box( |
| 78 | modifier = Modifier |
Ralston Da Silva | a19b9ec | 2020-12-03 16:09:45 -0800 | [diff] [blame] | 79 | .onFocusChanged { focusState = it } |
Ralston Da Silva | 71a5eaf | 2020-12-15 18:01:50 -0800 | [diff] [blame] | 80 | .focusRequester(focusRequester) |
Ralston Da Silva | 24cb7a0 | 2020-12-08 17:08:54 -0800 | [diff] [blame] | 81 | .focusModifier() |
Ralston Da Silva | ad70d26 | 2020-07-24 12:40:16 -0700 | [diff] [blame] | 82 | ) |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 83 | } |
| 84 | |
| 85 | // Act. |
Filip Pavlis | 375534f | 2020-09-03 16:59:33 +0100 | [diff] [blame] | 86 | rule.runOnIdle { |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 87 | ownerView.requestFocus() |
| 88 | } |
| 89 | |
| 90 | // Assert. |
Filip Pavlis | 375534f | 2020-09-03 16:59:33 +0100 | [diff] [blame] | 91 | rule.runOnIdle { |
Ralston Da Silva | ad70d26 | 2020-07-24 12:40:16 -0700 | [diff] [blame] | 92 | assertThat(focusState).isEqualTo(Active) |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 93 | } |
| 94 | } |
| 95 | |
| 96 | @Ignore("Enable this test after the owner propagates focus to the hierarchy (b/152535715)") |
| 97 | @Test |
| 98 | fun whenWindowGainsFocus_focusModifiersAreUpdated() { |
| 99 | // Arrange. |
Andrey Kulikov | 3832037 | 2020-05-07 19:55:05 +0100 | [diff] [blame] | 100 | lateinit var ownerView: View |
Ralston Da Silva | ad70d26 | 2020-07-24 12:40:16 -0700 | [diff] [blame] | 101 | var focusState = Inactive |
Ralston Da Silva | 71a5eaf | 2020-12-15 18:01:50 -0800 | [diff] [blame] | 102 | val focusRequester = FocusRequester() |
Filip Pavlis | 375534f | 2020-09-03 16:59:33 +0100 | [diff] [blame] | 103 | rule.setFocusableContent { |
Ralston Da Silva | bdaaf17 | 2021-01-11 14:23:39 -0800 | [diff] [blame] | 104 | ownerView = AmbientView.current |
Ralston Da Silva | ad70d26 | 2020-07-24 12:40:16 -0700 | [diff] [blame] | 105 | Box( |
| 106 | modifier = Modifier |
Ralston Da Silva | a19b9ec | 2020-12-03 16:09:45 -0800 | [diff] [blame] | 107 | .onFocusChanged { focusState = it } |
Ralston Da Silva | 71a5eaf | 2020-12-15 18:01:50 -0800 | [diff] [blame] | 108 | .focusRequester(focusRequester) |
Ralston Da Silva | 24cb7a0 | 2020-12-08 17:08:54 -0800 | [diff] [blame] | 109 | .focusModifier() |
Ralston Da Silva | ad70d26 | 2020-07-24 12:40:16 -0700 | [diff] [blame] | 110 | ) |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 111 | } |
| 112 | |
| 113 | // Act. |
Filip Pavlis | 375534f | 2020-09-03 16:59:33 +0100 | [diff] [blame] | 114 | rule.runOnIdle { |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 115 | ownerView.dispatchWindowFocusChanged(true) |
| 116 | } |
| 117 | |
| 118 | // Assert. |
Filip Pavlis | 375534f | 2020-09-03 16:59:33 +0100 | [diff] [blame] | 119 | rule.runOnIdle { |
Ralston Da Silva | ad70d26 | 2020-07-24 12:40:16 -0700 | [diff] [blame] | 120 | assertThat(focusState).isEqualTo(Active) |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 121 | } |
| 122 | } |
| 123 | |
| 124 | @Test |
| 125 | fun whenOwnerLosesFocus_focusModifiersAreUpdated() { |
| 126 | // Arrange. |
Andrey Kulikov | 3832037 | 2020-05-07 19:55:05 +0100 | [diff] [blame] | 127 | lateinit var ownerView: View |
Ralston Da Silva | ad70d26 | 2020-07-24 12:40:16 -0700 | [diff] [blame] | 128 | var focusState = Inactive |
Ralston Da Silva | 71a5eaf | 2020-12-15 18:01:50 -0800 | [diff] [blame] | 129 | val focusRequester = FocusRequester() |
Filip Pavlis | 375534f | 2020-09-03 16:59:33 +0100 | [diff] [blame] | 130 | rule.setFocusableContent { |
Ralston Da Silva | bdaaf17 | 2021-01-11 14:23:39 -0800 | [diff] [blame] | 131 | ownerView = AmbientView.current |
Ralston Da Silva | ad70d26 | 2020-07-24 12:40:16 -0700 | [diff] [blame] | 132 | Box( |
| 133 | modifier = Modifier |
Ralston Da Silva | a19b9ec | 2020-12-03 16:09:45 -0800 | [diff] [blame] | 134 | .onFocusChanged { focusState = it } |
Ralston Da Silva | 71a5eaf | 2020-12-15 18:01:50 -0800 | [diff] [blame] | 135 | .focusRequester(focusRequester) |
Ralston Da Silva | 24cb7a0 | 2020-12-08 17:08:54 -0800 | [diff] [blame] | 136 | .focusModifier() |
Ralston Da Silva | ad70d26 | 2020-07-24 12:40:16 -0700 | [diff] [blame] | 137 | ) |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 138 | } |
Filip Pavlis | 375534f | 2020-09-03 16:59:33 +0100 | [diff] [blame] | 139 | rule.runOnIdle { |
Ralston Da Silva | 71a5eaf | 2020-12-15 18:01:50 -0800 | [diff] [blame] | 140 | focusRequester.requestFocus() |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 141 | } |
| 142 | |
| 143 | // Act. |
Filip Pavlis | 375534f | 2020-09-03 16:59:33 +0100 | [diff] [blame] | 144 | rule.runOnIdle { |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 145 | ownerView.clearFocus() |
| 146 | } |
| 147 | |
| 148 | // Assert. |
Filip Pavlis | 375534f | 2020-09-03 16:59:33 +0100 | [diff] [blame] | 149 | rule.runOnIdle { |
Ralston Da Silva | ad70d26 | 2020-07-24 12:40:16 -0700 | [diff] [blame] | 150 | assertThat(focusState).isEqualTo(Inactive) |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 151 | } |
| 152 | } |
| 153 | |
| 154 | @Test |
| 155 | fun whenWindowLosesFocus_focusStateIsUnchanged() { |
| 156 | // Arrange. |
Andrey Kulikov | 3832037 | 2020-05-07 19:55:05 +0100 | [diff] [blame] | 157 | lateinit var ownerView: View |
Ralston Da Silva | ad70d26 | 2020-07-24 12:40:16 -0700 | [diff] [blame] | 158 | var focusState = Inactive |
Ralston Da Silva | 71a5eaf | 2020-12-15 18:01:50 -0800 | [diff] [blame] | 159 | val focusRequester = FocusRequester() |
Filip Pavlis | 375534f | 2020-09-03 16:59:33 +0100 | [diff] [blame] | 160 | rule.setFocusableContent { |
Ralston Da Silva | bdaaf17 | 2021-01-11 14:23:39 -0800 | [diff] [blame] | 161 | ownerView = AmbientView.current |
Ralston Da Silva | ad70d26 | 2020-07-24 12:40:16 -0700 | [diff] [blame] | 162 | Box( |
| 163 | modifier = Modifier |
Ralston Da Silva | a19b9ec | 2020-12-03 16:09:45 -0800 | [diff] [blame] | 164 | .onFocusChanged { focusState = it } |
Ralston Da Silva | 71a5eaf | 2020-12-15 18:01:50 -0800 | [diff] [blame] | 165 | .focusRequester(focusRequester) |
Ralston Da Silva | 24cb7a0 | 2020-12-08 17:08:54 -0800 | [diff] [blame] | 166 | .focusModifier() |
Ralston Da Silva | ad70d26 | 2020-07-24 12:40:16 -0700 | [diff] [blame] | 167 | ) |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 168 | } |
Filip Pavlis | 375534f | 2020-09-03 16:59:33 +0100 | [diff] [blame] | 169 | rule.runOnIdle { |
Ralston Da Silva | 71a5eaf | 2020-12-15 18:01:50 -0800 | [diff] [blame] | 170 | focusRequester.requestFocus() |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 171 | } |
| 172 | |
| 173 | // Act. |
Filip Pavlis | 375534f | 2020-09-03 16:59:33 +0100 | [diff] [blame] | 174 | rule.runOnIdle { |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 175 | ownerView.dispatchWindowFocusChanged(false) |
| 176 | } |
| 177 | |
| 178 | // Assert. |
Filip Pavlis | 375534f | 2020-09-03 16:59:33 +0100 | [diff] [blame] | 179 | rule.runOnIdle { |
Ralston Da Silva | ad70d26 | 2020-07-24 12:40:16 -0700 | [diff] [blame] | 180 | assertThat(focusState).isEqualTo(Active) |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 181 | } |
| 182 | } |
| 183 | |
Ralston Da Silva | 262f1d6 | 2021-01-06 03:09:02 -0800 | [diff] [blame] | 184 | @Test |
Ralston Da Silva | bdaaf17 | 2021-01-11 14:23:39 -0800 | [diff] [blame] | 185 | fun clickingOnNonClickableSpaceInAppWhenViewIsFocused_doesNotChangeViewFocus() { |
Ralston Da Silva | 262f1d6 | 2021-01-06 03:09:02 -0800 | [diff] [blame] | 186 | // Arrange. |
| 187 | val nonClickable = "notClickable" |
Ralston Da Silva | bdaaf17 | 2021-01-11 14:23:39 -0800 | [diff] [blame] | 188 | var didViewFocusChange = false |
Ralston Da Silva | 262f1d6 | 2021-01-06 03:09:02 -0800 | [diff] [blame] | 189 | lateinit var ownerView: View |
| 190 | rule.setFocusableContent { |
Ralston Da Silva | bdaaf17 | 2021-01-11 14:23:39 -0800 | [diff] [blame] | 191 | ownerView = AmbientView.current |
Ralston Da Silva | 262f1d6 | 2021-01-06 03:09:02 -0800 | [diff] [blame] | 192 | Box(Modifier.testTag(nonClickable)) |
| 193 | } |
Ralston Da Silva | bdaaf17 | 2021-01-11 14:23:39 -0800 | [diff] [blame] | 194 | rule.runOnIdle { |
| 195 | ownerView.requestFocus() |
| 196 | assertThat(ownerView.isFocused).isTrue() |
| 197 | } |
Ralston Da Silva | 262f1d6 | 2021-01-06 03:09:02 -0800 | [diff] [blame] | 198 | ownerView.setOnFocusChangeListener { _, hasFocus -> |
| 199 | if (hasFocus) { |
Ralston Da Silva | bdaaf17 | 2021-01-11 14:23:39 -0800 | [diff] [blame] | 200 | didViewFocusChange = true |
Ralston Da Silva | 262f1d6 | 2021-01-06 03:09:02 -0800 | [diff] [blame] | 201 | } |
| 202 | } |
| 203 | |
| 204 | // Act. |
| 205 | rule.onNodeWithTag(nonClickable).performClick() |
| 206 | |
| 207 | // Assert. |
| 208 | rule.runOnIdle { |
Ralston Da Silva | bdaaf17 | 2021-01-11 14:23:39 -0800 | [diff] [blame] | 209 | assertThat(didViewFocusChange).isFalse() |
Ralston Da Silva | 262f1d6 | 2021-01-06 03:09:02 -0800 | [diff] [blame] | 210 | assertThat(ownerView.isFocused).isTrue() |
| 211 | } |
| 212 | } |
| 213 | |
Ralston Da Silva | bdaaf17 | 2021-01-11 14:23:39 -0800 | [diff] [blame] | 214 | @Test |
| 215 | fun clickingOnNonClickableSpaceInAppWhenViewIsNotFocused_doesNotChangeViewFocus() { |
| 216 | // Arrange. |
| 217 | val nonClickable = "notClickable" |
| 218 | var didViewFocusChange = false |
| 219 | lateinit var ownerView: View |
| 220 | rule.setFocusableContent { |
| 221 | ownerView = AmbientView.current |
| 222 | Box(Modifier.testTag(nonClickable)) |
| 223 | } |
| 224 | rule.runOnIdle { assertThat(ownerView.isFocused).isFalse() } |
| 225 | ownerView.setOnFocusChangeListener { _, hasFocus -> |
| 226 | if (hasFocus) { |
| 227 | didViewFocusChange = true |
| 228 | } |
| 229 | } |
| 230 | |
| 231 | // Act. |
| 232 | rule.onNodeWithTag(nonClickable).performClick() |
| 233 | |
| 234 | // Assert. |
| 235 | rule.runOnIdle { |
| 236 | assertThat(didViewFocusChange).isFalse() |
| 237 | assertThat(ownerView.isFocused).isFalse() |
| 238 | } |
| 239 | } |
Ralston Da Silva | 1437364 | 2020-06-02 12:08:49 -0700 | [diff] [blame] | 240 | } |