Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: add UI tests
Browse files Browse the repository at this point in the history
descorp committed May 13, 2024
1 parent e0100aa commit bfb6ea4
Showing 7 changed files with 409 additions and 21 deletions.
15 changes: 15 additions & 0 deletions example/ios/AdyenExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
@@ -618,6 +618,7 @@
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION,
);
GCC_SYMBOLS_PRIVATE_EXTERN = NO;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@@ -638,6 +639,11 @@
);
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
OTHER_LDFLAGS = (
"$(inherited)",
"-Wl",
"-ld_classic",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
};
@@ -680,6 +686,10 @@
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "";
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
_LIBCPP_ENABLE_CXX17_REMOVED_UNARY_BINARY_FUNCTION,
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
@@ -697,6 +707,11 @@
"\"$(inherited)\"",
);
MTL_ENABLE_DEBUG_INFO = NO;
OTHER_LDFLAGS = (
"$(inherited)",
"-Wl",
"-ld_classic",
);
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
VALIDATE_PRODUCT = YES;
170 changes: 170 additions & 0 deletions example/ios/AdyenExampleTests/DropInNativeModuleTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
//
// Copyright (c) 2024 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import Adyen
@testable import adyen_react_native
import XCTest

final class DropInNativeModuleTests: XCTestCase {

let shortPaymentMethods: NSDictionary = ["paymentMethods": [
[
"type": "scheme",
"name": "Cards"
],
[
"type": "klarna",
"name": "Klarna"
],
]]

let fullPaymentMethods: NSDictionary = [
"paymentMethods": [
[
"type": "scheme",
"name": "Cards"
],
[
"type": "klarna",
"name": "Klarna"
],
],
"storedPaymentMethods": [
[
"brand": "visa",
"expiryMonth": "03",
"expiryYear": "30",
"id": "J469JCZC5KPBGP65",
"lastFour": "6746",
"name": "VISA",
"supportedShopperInteractions": [
"Ecommerce",
"ContAuth"
],
"type": "scheme"
],
]
]


func testSimpleList() throws {
// GIVEN
let sut = DropInModule()
let config: NSDictionary = ["clientKey": "live_XXXXXXXXXX"]


// WHEN
sut.open(shortPaymentMethods, configuration: config)

// THEN
XCTAssertTrue(try isPresentingDropIn())

let topView = try XCTUnwrap(getDropInView() as? UITableViewController)
XCTAssertEqual(getNumberOfElement(in: topView.tableView, section: 0), 2)
XCTAssertEqual(topView.title, "Payment Methods")

// TEAR DOWN
dissmissDropIn()
}

func testStoredList() throws {
// GIVEN
let sut = DropInModule()
let config: NSDictionary = [
"clientKey": "live_XXXXXXXXXX",
]

// WHEN
sut.open(fullPaymentMethods, configuration: config)

// THEN
XCTAssertTrue(try isPresentingDropIn())

let topView = try XCTUnwrap(getDropInView())
XCTAssertEqual(topView.title, "AdyenExample")

// TEAR DOWN
dissmissDropIn()
}

func testTitleSetter() throws {
// GIVEN
let sut = DropInModule()
let config: NSDictionary = [
"clientKey": "live_XXXXXXXXXX",
"dropin": [
"title": "MY_TITLE"
]
]

// WHEN
sut.open(fullPaymentMethods, configuration: config)

// THEN
XCTAssertTrue(try isPresentingDropIn())

let topView = try XCTUnwrap(getDropInView())
XCTAssertEqual(topView.title, "MY_TITLE")

// TEAR DOWN
dissmissDropIn()
}

func testSkipingPreset() throws {
// GIVEN
let sut = DropInModule()
let config: NSDictionary = [
"clientKey": "live_XXXXXXXXXX",
"dropin": [
"showPreselectedStoredPaymentMethod": false
]
]

// WHEN
sut.open(fullPaymentMethods, configuration: config)

// THEN
XCTAssertTrue(try isPresentingDropIn())

let topView = try XCTUnwrap(getDropInView() as? UITableViewController)
XCTAssertEqual(getNumberOfElement(in: topView.tableView, section: 0), 1)
XCTAssertEqual(getNumberOfElement(in: topView.tableView, section: 1), 2)
XCTAssertEqual(topView.title, "Payment Methods")

// TEAR DOWN
dissmissDropIn()
}

func isPresentingDropIn() throws -> Bool {
let dropin = try waitUntilTopPresenter(isOfType: UINavigationController.self)
return dropin is AdyenObserver
}

func getDropInView() -> UIViewController? {

var controller: UIViewController?
var nextController: UIViewController? = UIApplication.shared.keyWindow?.rootViewController?.presentedViewController

while nextController != nil {
controller = nextController
nextController = nextController?.children.first
}
return controller
}

func getNumberOfElement(in tableView: UITableView, section: Int) -> Int? {
tableView.dataSource?.tableView(tableView, numberOfRowsInSection: section)
}

func dissmissDropIn() {
let expectation = expectation(description: "DropIn closing")
UIApplication.shared.keyWindow?.rootViewController?.dismiss(animated: false, completion: {
expectation.fulfill()
})
wait(for: [expectation])
}

}
37 changes: 37 additions & 0 deletions example/ios/AdyenExampleTests/UIView+Search.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// Copyright (c) 2024 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import UIKit

internal extension UIView {
func findView<T: UIView>(with accessibilityIdentifier: String) -> T? {
if self.accessibilityIdentifier == accessibilityIdentifier {
return self as? T
}

for subview in subviews {
if let v = subview.findView(with: accessibilityIdentifier) {
return v as? T
}
}

return nil
}

func findView<T: UIView>(by lastAccessibilityIdentifierComponent: String) -> T? {
if self.accessibilityIdentifier?.hasSuffix(lastAccessibilityIdentifierComponent) == true {
return self as? T
}

for subview in subviews {
if let v = subview.findView(by: lastAccessibilityIdentifierComponent) {
return v as? T
}
}

return nil
}
}
44 changes: 44 additions & 0 deletions example/ios/AdyenExampleTests/UIViewController+Search.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// Copyright (c) 2024 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import UIKit
import XCTest
@_spi(AdyenInternal) @testable import Adyen

public extension UIViewController {

/// Returns the first child of the viewControllers children that matches the type
///
/// - Parameters:
/// - type: The type of the viewController
func firstChild<T: UIViewController>(of type: T.Type) -> T? {
if let result = self as? T {
return result
}

for child in self.children {
if let result = child.firstChild(of: T.self) {
return result
}
}

return nil
}

/// Returns the current top viewController
///
/// - Throws: if there is no rootViewController can be found on the window
static func topPresenter() throws -> UIViewController {
let rootViewController = try XCTUnwrap(UIApplication.shared.adyen.mainKeyWindow?.rootViewController)
return rootViewController.adyen.topPresenter
}
}

extension UIViewController: PresentationDelegate {
public func present(component: PresentableComponent) {
self.present(component.viewController, animated: false, completion: nil)
}
}
60 changes: 60 additions & 0 deletions example/ios/AdyenExampleTests/Wait+UIKit.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// Copyright (c) 2024 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import UIKit
import XCTest

extension XCTestCase {

/// Waits for a viewController of a certain type to become a child of another viewController
///
/// Instead of waiting for a specific amount of time it polls if the expecation is returning true in time intervals of 10ms until the timeout is reached.
/// Use it whenever a value change is not guaranteed to be instant or happening after a short amount of time.
///
/// - Parameters:
/// - ofType: the type of the expected child viewController
/// - viewController: the parent viewController
/// - timeout: the maximum time (in seconds) to wait.
@discardableResult
func waitForViewController<T: UIViewController>(
ofType: T.Type,
toBecomeChildOf viewController: UIViewController,
timeout: TimeInterval = 60
) throws -> T {

wait(
until: { viewController.firstChild(of: T.self) != nil },
timeout: timeout,
message: "\(String(describing: T.self)) should appear on \(String(describing: viewController.self)) before timeout \(timeout)s"
)

return try XCTUnwrap(viewController.firstChild(of: T.self))
}

/// Waits for a viewController of a certain type to become a child of another viewController
///
/// Instead of waiting for a specific amount of time it polls if the expecation is returning true in time intervals of 10ms until the timeout is reached.
/// Use it whenever a value change is not guaranteed to be instant or happening after a short amount of time.
///
/// - Parameters:
/// - ofType: the type of the expected child viewController
/// - viewController: the parent viewController
/// - timeout: the maximum time (in seconds) to wait.
@discardableResult
func waitUntilTopPresenter<T: UIViewController>(
isOfType: T.Type,
timeout: TimeInterval = 60
) throws -> T {

wait(
until: { (try? UIViewController.topPresenter() is T) ?? false },
timeout: timeout,
message: "\(String(describing: T.self)) should become top presenter before timeout \(timeout)s"
)

return try XCTUnwrap(UIViewController.topPresenter() as? T)
}
}
62 changes: 62 additions & 0 deletions example/ios/AdyenExampleTests/Wait.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//
// Copyright (c) 2024 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import XCTest

extension XCTestCase {

/// Waits until a certain condition is met
///
/// Instead of waiting for a specific amount of time it polls if the expecation is returning true in time intervals of 10ms until the timeout is reached.
/// Use it whenever a value change is not guaranteed to be instant or happening after a short amount of time.
///
/// - Parameters:
/// - expectation: the condition that is waited on
/// - timeout: the maximum time (in seconds) to wait.
/// - retryInterval: the waiting time inbetween retries
/// - message: an optional message on failure
func wait(
until expectation: () -> Bool,
timeout: TimeInterval = 60,
retryInterval: DispatchTimeInterval = .seconds(1),
message: String? = nil
) {
let thresholdDate = Date().addingTimeInterval(timeout)

var isMatchingExpectation = expectation()

while thresholdDate.timeIntervalSinceNow > 0, !isMatchingExpectation {
wait(for: retryInterval)
isMatchingExpectation = expectation()
}

XCTAssertTrue(isMatchingExpectation, message ?? "Expectation should be met before timeout \(timeout)s")
}

/// Waits until a keyPath of a target matches an expected value
///
/// Instead of waiting for a specific amount of time it polls if the expecation is returning true in time intervals of 10ms until the timeout is reached.
/// Use it whenever a value change is not guaranteed to be instant or happening after a short amount of time.
///
/// - Parameters:
/// - target: the target to observe
/// - keyPath: the keyPath to check
/// - expectedValue: the value to check against
/// - timeout: the maximum time (in seconds) to wait.
func wait<Value: Equatable, Target: AnyObject>(
until target: Target,
at keyPath: KeyPath<Target, Value>,
is expectedValue: Value,
timeout: TimeInterval = 60,
line: Int = #line
) {
wait(
until: { target[keyPath: keyPath] == expectedValue },
timeout: timeout,
message: "Value of \(keyPath) (\(target[keyPath: keyPath])) should become \(String(describing: expectedValue)) within \(timeout)s [line:\(line)]"
)
}
}
42 changes: 21 additions & 21 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,38 +1,38 @@
PODS:
- Adyen (5.7.0):
- Adyen/Actions (= 5.7.0)
- Adyen/Card (= 5.7.0)
- Adyen/Components (= 5.7.0)
- Adyen/Core (= 5.7.0)
- Adyen/DropIn (= 5.7.0)
- Adyen/Encryption (= 5.7.0)
- Adyen/Session (= 5.7.0)
- Adyen (5.7.1):
- Adyen/Actions (= 5.7.1)
- Adyen/Card (= 5.7.1)
- Adyen/Components (= 5.7.1)
- Adyen/Core (= 5.7.1)
- Adyen/DropIn (= 5.7.1)
- Adyen/Encryption (= 5.7.1)
- Adyen/Session (= 5.7.1)
- adyen-react-native (2.0.0-local.1):
- Adyen (= 5.7.0)
- Adyen (= 5.7.1)
- React-Core
- Adyen/Actions (5.7.0):
- Adyen/Actions (5.7.1):
- Adyen/Core
- Adyen3DS2 (= 2.4.1)
- Adyen/Card (5.7.0):
- Adyen3DS2 (= 2.4.2)
- Adyen/Card (5.7.1):
- Adyen/Core
- Adyen/Encryption
- Adyen/Components (5.7.0):
- Adyen/Components (5.7.1):
- Adyen/Core
- Adyen/Encryption
- Adyen/Core (5.7.0):
- Adyen/Core (5.7.1):
- AdyenNetworking (= 2.0.0)
- Adyen/DropIn (5.7.0):
- Adyen/DropIn (5.7.1):
- Adyen/Actions
- Adyen/Card
- Adyen/Components
- Adyen/Core
- Adyen/Encryption
- Adyen/Encryption (5.7.0):
- Adyen/Encryption (5.7.1):
- Adyen/Core
- Adyen/Session (5.7.0):
- Adyen/Session (5.7.1):
- Adyen/Actions
- Adyen/Core
- Adyen3DS2 (2.4.1)
- Adyen3DS2 (2.4.2)
- AdyenNetworking (2.0.0)
- boost (1.76.0)
- DoubleConversion (1.1.6)
@@ -482,9 +482,9 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"

SPEC CHECKSUMS:
Adyen: 48550990ad15ccde06ca6f8dc3143fc8c44719b6
adyen-react-native: f45e5095ed4711680eeb9d95afa93d5c1d664b5b
Adyen3DS2: 6e1e6c7369118377feabe59706fc4a06dcfa848b
Adyen: 61b166d1412eec226e8aa00727b7dc25d25008bb
adyen-react-native: 2f82afdab97be28fbe926a9cdfa8bf87b1c81c1b
Adyen3DS2: aaa0a86c89f4f5d482d20894b2f91e91a63aee6b
AdyenNetworking: 49e447632b542e08c6dd06bb47ea1077a8adecb7
boost: 57d2868c099736d80fcd648bf211b4431e51a558
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54

0 comments on commit bfb6ea4

Please sign in to comment.