Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: iOS unit tests #366

Merged
merged 15 commits into from
Mar 29, 2024
Merged
23 changes: 23 additions & 0 deletions .github/workflows/verify-ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,26 @@ jobs:

- name: Verify formatting
run: yarn lint-clang
unit-tests:
name: 📖 Unit tests
runs-on: macOS-14
defaults:
run:
working-directory: ./ios/KeyboardControllerNative
steps:
- uses: actions/checkout@v4

- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "15.3"

- name: Install xcpretty
run: gem install xcpretty

- name: Run unit tests
run: "set -o pipefail && xcodebuild \
test \
-scheme KeyboardControllerNative \
-only-testing KeyboardControllerNativeTests \
-destination 'platform=iOS Simulator,name=iPhone 15 Pro' \
CODE_SIGNING_ALLOWED=NO | xcpretty"
2 changes: 1 addition & 1 deletion FabricExample/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1597,7 +1597,7 @@ SPEC CHECKSUMS:
React-jsitracing: 4fed160d939e93a39049481f47744af246a7ac2c
React-logger: 3eb80a977f0d9669468ef641a5e1fabbc50a09ec
React-Mapbuffer: 84ea43c6c6232049135b1550b8c60b2faac19fab
react-native-keyboard-controller: 7d37211044d15aa8178b04df9a85263f52a07203
react-native-keyboard-controller: 779693f3474f963ee1d9df1d965368ba82c88abf
react-native-safe-area-context: 1e374c51edf537be56313b893b6e96b0e254ddfe
React-nativeconfig: b4d4e9901d4cabb57be63053fd2aa6086eb3c85f
React-NativeModulesApple: cd26e56d56350e123da0c1e3e4c76cb58a05e1ee
Expand Down
2 changes: 1 addition & 1 deletion example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1405,7 +1405,7 @@ SPEC CHECKSUMS:
React-jsinspector: 85583ef014ce53d731a98c66a0e24496f7a83066
React-logger: 3eb80a977f0d9669468ef641a5e1fabbc50a09ec
React-Mapbuffer: 84ea43c6c6232049135b1550b8c60b2faac19fab
react-native-keyboard-controller: 27a2f1da4a1fedf9f01bf49e2dcabba700daa284
react-native-keyboard-controller: e0f1b0b71a76e05c99fc6bfdd8b8ec05bead4105
react-native-safe-area-context: b97eb6f9e3b7f437806c2ce5983f479f8eb5de4b
react-native-text-input-mask: 22ca8eeef84d42a896f79428f7d175a5eb8b1c4e
React-nativeconfig: b4d4e9901d4cabb57be63053fd2aa6086eb3c85f
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"colors": [
{
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"images": [
{
"idiom": "universal",
"platform": "ios",
"size": "1024x1024"
}
],
"info": {
"author": "xcode",
"version": 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info": {
"author": "xcode",
"version": 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// ContentView.swift
// KeyboardControllerNative
//
// Created by Kiryl Ziusko on 29/03/2024.
//

import SwiftUI

struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
}
.padding()
}
}

#Preview {
ContentView()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// Extension+UIView.swift
// Tests
//
// Created by Kiryl Ziusko on 21/02/2024.
//

import Foundation
import UIKit

public extension UIView {
var reactTag: NSNumber {
return tag as NSNumber
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// KeyboardControllerNativeApp.swift
// KeyboardControllerNative
//
// Created by Kiryl Ziusko on 29/03/2024.
//

import SwiftUI

@main
struct KeyboardControllerNativeApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info": {
"author": "xcode",
"version": 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
//
// KeyboardControllerNativeTests.swift
// KeyboardControllerNativeTests
//
// Created by Kiryl Ziusko on 29/03/2024.
//

@testable import KeyboardControllerNative
import XCTest

extension XCTestCase {
func waitForFocusChange(
to textField: TestableInput,
timeout: TimeInterval = 10.0,
file: StaticString = #file,
line: UInt = #line
) {
let expectation = XCTestExpectation(description: "Wait for focus change to \(textField.tag)")

XCTAssertFalse(
textField.becomeFirstResponderCalled,
"Expected focus shouldn't be initially set for tag \(textField.tag)"
)

DispatchQueue.main.async {
XCTAssertTrue(
textField.becomeFirstResponderCalled,
"Expected focus to be set to text field with tag \(textField.tag)",
file: file,
line: line
)
expectation.fulfill()
}

wait(for: [expectation], timeout: timeout)
}
}

protocol TestableInput: UIView, TextInput {
var becomeFirstResponderCalled: Bool { get set }
func becomeFirstResponder() -> Bool
}

class TestableTextField: UITextField, TestableInput {
var becomeFirstResponderCalled = false

override func becomeFirstResponder() -> Bool {
becomeFirstResponderCalled = true
return super.becomeFirstResponder()
}
}

class TestableTextView: UITextView, TestableInput {
var becomeFirstResponderCalled = false

override func becomeFirstResponder() -> Bool {
becomeFirstResponderCalled = true
return super.becomeFirstResponder()
}
}

final class KeyboardControllerNativeTests: XCTestCase {
var rootView: UIView!
var textFields: [TestableInput]!

override func setUpWithError() throws {
super.setUp()

rootView = UIView()
textFields = (1 ... 13).map { tag in
let textField = (tag % 2 == 0 ? TestableTextField() : TestableTextView()) as TestableInput
textField.tag = tag
let isEditable = tag != 3 && tag != 4 // Assuming ids 3 and 4 are not editable, similar to our Android test
(textField as? UITextField)?.isEnabled = isEditable
(textField as? UITextView)?.isEditable = isEditable

return textField
}

let subView = UIView()
for (index, textField) in textFields.enumerated() {
if index == 4 {
rootView.addSubview(subView)
}
if index >= 4, index <= 6 {
subView.addSubview(textField)
} else {
rootView.addSubview(textField)
}
}
}

override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}

func testSetFocusToNextShouldSetFocusToNextField() throws {
let textInput1 = textFields[0]
FocusedInputHolder.shared.set(textInput1)

ViewHierarchyNavigator.setFocusTo(direction: "next")

waitForFocusChange(to: textFields[1])
}

func testSetFocusToPrevShouldSetFocusToPreviousField() throws {
let textInput2 = textFields[1]
FocusedInputHolder.shared.set(textInput2)

ViewHierarchyNavigator.setFocusTo(direction: "prev")

waitForFocusChange(to: textFields[0])
}

func testSetFocusToNextShouldSkipNonEditableFields() throws {
let textInput2 = textFields[1]
FocusedInputHolder.shared.set(textInput2)

ViewHierarchyNavigator.setFocusTo(direction: "next")

waitForFocusChange(to: textFields[4])
}

func testSetFocusToPrevShouldSkipNonEditableFields() throws {
let textInput5 = textFields[4]
FocusedInputHolder.shared.set(textInput5)

ViewHierarchyNavigator.setFocusTo(direction: "prev")

waitForFocusChange(to: textFields[1])
}

func testSetFocusToNextWithinGroup() throws {
let textInput5 = textFields[4]
FocusedInputHolder.shared.set(textInput5)

ViewHierarchyNavigator.setFocusTo(direction: "next")

waitForFocusChange(to: textFields[5])
}

func testSetFocusToPrevWithinGroup() throws {
let textInput6 = textFields[5]
FocusedInputHolder.shared.set(textInput6)

ViewHierarchyNavigator.setFocusTo(direction: "prev")

waitForFocusChange(to: textFields[4])
}

func testSetFocusToNextExitsGroup() throws {
let textInput7 = textFields[6]
FocusedInputHolder.shared.set(textInput7)

ViewHierarchyNavigator.setFocusTo(direction: "next")

waitForFocusChange(to: textFields[7])
}

func testSetFocusToPrevEntersGroupAtLastElement() throws {
let textInput8 = textFields[7]
FocusedInputHolder.shared.set(textInput8)

ViewHierarchyNavigator.setFocusTo(direction: "prev")

waitForFocusChange(to: textFields[6])
}

func testPerformanceExample() throws {
// This is an example of a performance test case.
measure {
// Put the code you want to measure the time of here.
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// KeyboardControllerNativeUITests.swift
// KeyboardControllerNativeUITests
//
// Created by Kiryl Ziusko on 29/03/2024.
//

import XCTest

final class KeyboardControllerNativeUITests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.

// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false

// In UI tests it’s important to set the initial state - such as interface orientation,
// which required for your tests before they run. The setUp method is a good place to do this.
}

override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}

func testExample() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()

// Use XCTAssert and related functions to verify your tests produce the correct results.
}

func testLaunchPerformance() throws {
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
// This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// KeyboardControllerNativeUITestsLaunchTests.swift
// KeyboardControllerNativeUITests
//
// Created by Kiryl Ziusko on 29/03/2024.
//

import XCTest

// swiftlint:disable:next type_name
final class KeyboardControllerNativeUITestsLaunchTests: XCTestCase {
override class var runsForEachTargetApplicationUIConfiguration: Bool {
true
}

override func setUpWithError() throws {
continueAfterFailure = false
}

func testLaunch() throws {
let app = XCUIApplication()
app.launch()

// Insert steps here to perform after app launch but before taking a screenshot,
// such as logging into a test account or navigating somewhere in the app

let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = "Launch Screen"
attachment.lifetime = .keepAlways
add(attachment)
}
}
Loading
Loading