Skip to content

Commit

Permalink
tests: FocusedInputHolder (Android) (#370)
Browse files Browse the repository at this point in the history
## 馃摐 Description

Added tests to `FocusedInputHolder`. Refactored code, reduced
duplications, aligned to JS-based namings.

## 馃挕 Motivation and Context

I decided to fully cover a new functionality with unit tests. The
`ViewHierarchyTraversal` was already covered, so I decided to cover
other classes. The next one `FocusedInputHolder`. This is a very simple
class, so it contains two unit tests:
- check that weak references implemented correctly;
- check that `focus` actually calls expected methods.

Along with that I did some refactoring: reduced code fragmentation,
reduced code duplication, aligned API to look similar to RN JS API (and
hide internals).

## 馃摙 Changelog

### iOS

- use `focus` instead of `requestFocus`;
- `FocusedInputHolder.shared.requestFocus()` ->
`FocusedInputHolder.shared.focus()`;

### Android

- added `FocusedInputHolderTests`
- added `focus` extension to avoid code duplication;
- use `focus` extension instead of `requestFocus`/`requestFocusFromJS`;
- `FocusedInputHolder.requestFocus()` -> `FocusedInputHolder.focus()`;
- `FocusedInputHolder` works with plain `EditText` (i. e. no
react-specific code);
- added `get` to `FocusedInputHolder`;

## 馃 How Has This Been Tested?

Tested via CI.

## 馃摑 Checklist

- [x] CI successfully passed
- [x] I added new mocks and corresponding unit-tests if library API was
changed
  • Loading branch information
kirillzyusko committed Feb 26, 2024
1 parent e87de70 commit 667351b
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 32 deletions.
Expand Up @@ -86,3 +86,11 @@ val EditText.parentScrollViewTarget: Int
// ScrollView was not found
return -1
}

fun EditText?.focus() {
if (this is ReactEditText) {
this.requestFocusFromJS()
} else {
this?.requestFocus()
}
}
Expand Up @@ -32,7 +32,7 @@ class KeyboardControllerModuleImpl(private val mReactContext: ReactApplicationCo

fun setFocusTo(direction: String) {
if (direction == "current") {
return FocusedInputHolder.requestFocus()
return FocusedInputHolder.focus()
}

val activity = mReactContext.currentActivity
Expand Down
@@ -1,16 +1,21 @@
package com.reactnativekeyboardcontroller.traversal

import com.facebook.react.views.textinput.ReactEditText
import android.widget.EditText
import com.reactnativekeyboardcontroller.extensions.focus
import java.lang.ref.WeakReference

object FocusedInputHolder {
private var input: WeakReference<ReactEditText?>? = null
private var input: WeakReference<EditText?>? = null

fun set(textInput: ReactEditText) {
fun set(textInput: EditText) {
input = WeakReference(textInput)
}

fun requestFocus() {
input?.get()?.requestFocusFromJS()
fun get(): EditText? {
return input?.get()
}

fun focus() {
input?.get()?.focus()
}
}
Expand Up @@ -4,18 +4,14 @@ import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.views.textinput.ReactEditText
import com.reactnativekeyboardcontroller.extensions.focus

object ViewHierarchyNavigator {
fun setFocusTo(direction: String, view: View) {
val input = if (direction == "next") findNextEditText(view) else findPreviousEditText(view)

UiThreadUtil.runOnUiThread {
if (input is ReactEditText) {
input.requestFocusFromJS()
} else {
input?.requestFocus()
}
input.focus()
}
}

Expand Down
@@ -0,0 +1,45 @@
package com.reactnativekeyboardcontroller.traversal

import android.content.Context
import android.widget.EditText
import androidx.test.core.app.ApplicationProvider
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@RunWith(RobolectricTestRunner::class)
class FocusedInputHolderTest {
@Test
fun `FocusedInputHolder should hold a weak reference`() {
val context = ApplicationProvider.getApplicationContext<Context>()
var input: EditText? = EditText(context)

FocusedInputHolder.set(input as EditText)

assertEquals(FocusedInputHolder.get(), input)

input = null

@Suppress("detekt:ExplicitGarbageCollectionCall")
System.gc()

assertNull(FocusedInputHolder.get())
}

@Test
fun `focus() should request focus on expected field`() {
val context = ApplicationProvider.getApplicationContext<Context>()
val input = EditText(context)

assertFalse(input.hasFocus())

FocusedInputHolder.set(input)
FocusedInputHolder.focus()

assertTrue(input.hasFocus())
}
}
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import android.widget.EditText
import android.widget.LinearLayout
import androidx.test.core.app.ApplicationProvider
import com.reactnativekeyboardcontroller.extensions.focus
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
Expand Down Expand Up @@ -78,7 +79,7 @@ class ViewHierarchyNavigatorTest {

@Test
fun `setFocusTo to 'next' should set focus to next field`() {
editText1.requestFocus()
editText1.focus()

ViewHierarchyNavigator.setFocusTo("next", editText1)

Expand All @@ -89,7 +90,7 @@ class ViewHierarchyNavigatorTest {

@Test
fun `setFocusTo to 'prev' should set focus to previous field`() {
editText2.requestFocus()
editText2.focus()

ViewHierarchyNavigator.setFocusTo("prev", editText2)

Expand All @@ -100,7 +101,7 @@ class ViewHierarchyNavigatorTest {

@Test
fun `setFocusTo to 'next' should skip non-editable fields`() {
editText2.requestFocus()
editText2.focus()

ViewHierarchyNavigator.setFocusTo("next", editText2)

Expand All @@ -111,7 +112,7 @@ class ViewHierarchyNavigatorTest {

@Test
fun `setFocusTo to 'prev' should skip non-editable fields`() {
editText5.requestFocus()
editText5.focus()

ViewHierarchyNavigator.setFocusTo("prev", editText5)

Expand All @@ -122,7 +123,7 @@ class ViewHierarchyNavigatorTest {

@Test
fun `setFocusTo to 'next' should set focus relatively to current group`() {
editText5.requestFocus()
editText5.focus()

ViewHierarchyNavigator.setFocusTo("next", editText5)

Expand All @@ -133,7 +134,7 @@ class ViewHierarchyNavigatorTest {

@Test
fun `setFocusTo to 'prev' should set focus relatively to current group`() {
editText7.requestFocus()
editText7.focus()

ViewHierarchyNavigator.setFocusTo("prev", editText7)

Expand All @@ -144,7 +145,7 @@ class ViewHierarchyNavigatorTest {

@Test
fun `setFocusTo to 'next' should correctly exit from current group`() {
editText7.requestFocus()
editText7.focus()

ViewHierarchyNavigator.setFocusTo("next", editText7)

Expand All @@ -155,7 +156,7 @@ class ViewHierarchyNavigatorTest {

@Test
fun `setFocusTo to 'prev' should set focus to last element in group`() {
editText8.requestFocus()
editText8.focus()

ViewHierarchyNavigator.setFocusTo("prev", editText8)

Expand All @@ -166,7 +167,7 @@ class ViewHierarchyNavigatorTest {

@Test
fun `setFocusTo to 'next' should do nothing if it's last element`() {
editText13.requestFocus()
editText13.focus()

ViewHierarchyNavigator.setFocusTo("next", editText13)

Expand All @@ -177,7 +178,7 @@ class ViewHierarchyNavigatorTest {

@Test
fun `setFocusTo to 'prev' should do nothing if it's first element`() {
editText1.requestFocus()
editText1.focus()

ViewHierarchyNavigator.setFocusTo("prev", editText1)

Expand Down
10 changes: 5 additions & 5 deletions ios/traversal/FocusedInputHolder.swift
Expand Up @@ -19,14 +19,14 @@ class FocusedInputHolder {
currentFocusedInput = input
}

// Requests focus for the currentFocusedInput if it's set
func requestFocus() {
currentFocusedInput?.requestFocus()
}

func get() -> TextInput? {
return currentFocusedInput
}

// Requests focus for the currentFocusedInput if it's set
func focus() {
currentFocusedInput?.focus()
}

private init() {}
}
6 changes: 3 additions & 3 deletions ios/traversal/TextInput.swift
Expand Up @@ -10,17 +10,17 @@ import Foundation
import UIKit

public protocol TextInput: AnyObject {
func requestFocus()
func focus()
}

extension UITextField: TextInput {
public func requestFocus() {
public func focus() {
becomeFirstResponder()
}
}

extension UITextView: TextInput {
public func requestFocus() {
public func focus() {
becomeFirstResponder()
}
}
4 changes: 2 additions & 2 deletions ios/traversal/ViewHierarchyNavigator.swift
Expand Up @@ -14,15 +14,15 @@ public class ViewHierarchyNavigator: NSObject {
@objc public static func setFocusTo(direction: String) {
DispatchQueue.main.async {
if direction == "current" {
FocusedInputHolder.shared.requestFocus()
FocusedInputHolder.shared.focus()
return
}

let input = (FocusedInputHolder.shared.get() as? UIView) ?? (UIResponder.current as? UIView) ?? nil
guard let view = input else { return }

let textField = findTextInputInDirection(currentFocus: view, direction: direction)
textField?.requestFocus()
textField?.focus()
}
}

Expand Down

0 comments on commit 667351b

Please sign in to comment.