/Einstein

Einstein is an UITest framework that integrates the logic across the Project and UITest through AccessibilityIdentified. And in UITest, using it to better support test code writing.

Primary LanguageSwiftMIT LicenseMIT


Documentation Version CI Status License Platform CI Status

Einstein is an UITest framework which integrates the business logic across the Project and UITest through AccessibilityIdentifier. And on UITest, using EasyPredict and Extensions to better support UITest code writing

Comparative sample

in XCTestCase, type the phone number to login

👍 Use Einstein ↓

LoginAccessID.SignIn.phoneNumber.element
  .assertBreak(predicate: .exists(true))?
  .clearAndType(text: "MyPhoneNumber")

😵 without Einstein ↓

let element = app.buttons["LoginAccessID_SignIn_phoneNumber"]
let predicate = NSPredicate(format: "exists == true")
let promise = self.expectation(for: predicate, evaluatedWith: element, handler: nil)
let result = XCTWaiter().wait(for: [promise], timeout: 10)
if result == XCTWaiter.Result.completed {
    let stringValue = (element.value as? String) ?? ""
    let deleteString = stringValue.map { _ in XCUIKeyboardKey.delete.rawValue }.joined()
    element.typeText(deleteString)
    element.typeText("MyPhoneNumber")
} else {
    assertionFailure("LoginAccessID_SignIn_phoneNumber element is't existe")
}

File structures

─┬─ Einstein
 ├─┬─ Identifier: -> `UIKit`
 │ └─── AccessibilityIdentifier.swift
 │
 └─┬─ UITest: -> `Einstein/Identifier` & `XCTest` & `Then`
   ├─┬─ Model
   │ ├─── EasyPredicate.swift
   │ └─── Springboard.swift
   └─┬─ Extensions
     ├─── RawRepresentable+helpers.swift
     ├─── PrettyRawRepresentable+helpers.swift
     ├─── XCTestCase+helpers.swift
     ├─── XCUIElement+helpers.swift
     └─── XCUIElementQuery+helpers.swift

Install

required iOS >= 9.0 Swift5.0 with Cocoapods

target 'XXXProject' do

  # in project target
  pod 'Einstein/Identifier' 
  
  target 'XXXProjectUITests' do
    # in UITest target
    pod 'Einstein'
  end
end

Using

  • AccessibilityIdentifier
    • Project target
    • UITest target
    • Apply in UITest
  • EasyPredicate
  • Extensions

1. AccessibilityIdentifier

Note:
all the UIKit's accessibilityIdentifier is a preperty of the protocol UIAccessibilityIdentification and all enum's rawValue is default to follow RawRepresentable

Expand for steps details
  • 1.1 Define the enums
    • set rawValue in String
    • append PrettyRawRepresentable if need
  • 1.2 set UIKit's accessibilityIdentifier by enums's rawValue
    • method1: infix operator
    • method2: UIAccessibilityIdentification's extension
  • 1.3 Apply in UITest target

1.1 Define the enums

struct LoginAccessID {
    enum SignIn: String {
        case signIn, phoneNumber, password
    }
    enum SignUp: String {
        case signUp, phoneNumber
    }
    enum Forget: String, PrettyRawRepresentable {
        case phoneNumber // and so on
    }
}

I highly recommend adding PrettyRawRepresentable protocol on enums, then you will get the RawValue string with the property path to avoid accessibilityIdentifier be samed in diff pages.

// for example:

let str1 = LoginAccessID.SignIn.phoneNumber
let str2 = LoginAccessID.SignUp.phoneNumber
let str3 = LoginAccessID.Forget.phoneNumber // had add PrettyRawRepresentable

str1 == "phoneNumber"
str2 == "phoneNumber" 
str3 == "LoginAccessID_Forget_phoneNumber"

see more: PrettyRawRepresentable

1.2 set UIKit's accessibilityIdentifier by enums's rawValue

// system way
signInPhoneTextField.accessibilityIdentifier = "LoginAccessID_SignIn_phoneNumber"

// define infix operator <<<
forgetPhoneTextField <<< LoginAccessID.Forget.phoneNumber

print(forgetPhoneTextField.accessibilityIdentifier)
// "LoginAccessID_Forget_phoneNumber"

1.3. Apply in UITest target

Note:
Firstly Import the defined enums file in UITest

  • Method 1: Set it's target membership as true both in XXXProject and XXXUITest
  • Method 2: Import project files in UITest with @testable Link: how to set
@testable import XXXPreject
// extension the protocol RawRepresentable and it's RawValue == String

typealias SignInPage = LoginAccessID.SignIn

// type the phone number
SignInPage.phoneNumber.element.waitUntilExists().clearAndType(text: "myPhoneNumber")

// type passward
SignInPage.password.element.clearAndType(text: "******")

// start login
SignInPage.signIn.element.assert(predicate: .isEnabled(true)).tap()

2. EasyPredicate

Note:
EasyPredicate's RawValue is PredicateRawValue (a another enum to manage logic and convert NSPredicate).

Expand for EasyPredicate's cases
public enum EasyPredicate: RawRepresentable {   
    case exists(_ exists: Bool)
    case isEnabled(_ isEnabled: Bool)
    case isHittable(_ isHittable: Bool)
    case isSelected(_ isSelected: Bool)
    case label(_ comparison: Comparison, _ value: String)
    case identifier(_ identifier: String)
    case type(_ type: XCUIElement.ElementType)
    case other(_ ragular: String)
}

Although NSPredicate is powerful, the developer program interface is not good enough, we can try to convert the hard code style into the object-oriented style. and this is what EasyPredicate do

// use EasyPredicate
let targetElement = query.filter(predicate: .label(.beginsWith, "abc")).element

// use NSPredicate
let predicate = NSPredicate(format: "label BEGINSWITH 'abc'")
let targetElement = query.element(matching: predicate).element

EasyPredicate Merge

// "elementType == 0 && exists == true && label BEGINSWITH 'abc'"
let predicate: EasyPredicate = [.type(.button), .exists(true), .label(.beginsWith, "abc")].merged()

// "elementType == 0 || exists == true || label BEGINSWITH 'abc'"
let predicate: EasyPredicate = [.type(.button), .exists(true), .label(.beginsWith, "abc")].merged(withLogic: .or)

3. UITest Extensions

3.1 extension String

/*
 Note: string value can be a RawRepresentable and String at the same time
 for example:
 `let element: XCUIElement = "SomeString".element`
 */
extension String: RawRepresentable {
    public var rawValue: String { return self }
    public init?(rawValue: String) {
        self = rawValue
    }
}

3.2 extension RawRepresentable

Expand for Sequence where Element: RawRepresentable
public extension Sequence where Element: RawRepresentable, Element.RawValue == String {
    
    /// get the elements which match with identifiers and predicates limited in timeout
    ///
    /// - Parameters:
    ///   - predicates: predicates as the match rules
    ///   - logic: relation of predicates
    ///   - timeout: if timeout == 0, return the elements immediately otherwise retry until timeout
    /// - Returns: get the elements
    func elements(predicates: [EasyPredicate], logic: NSCompoundPredicate.LogicalType, timeout: Int) -> [XCUIElement] {}
    
    /// get the first element was matched predicate
    func anyElement(predicate: EasyPredicate) -> XCUIElement? {}
}
Expand for RawRepresentable extension
/*
 Get the `XCUIElement` from RawRepresentable's RawValue which also been used as accessibilityIdentifier
 */
public extension RawRepresentable where RawValue == String {
    var element: XCUIElement {}
    var query: XCUIElementQuery {}
    var count: Int {}
    subscript(i: Int) -> XCUIElement {}   
    func queryFor(identifier: Self) -> XCUIElementQuery {}
}

3.3 extension XCUIElement

Expand for XCUIElement (Base)
public extension PredicateBaseExtensionProtocol where Self == T {

    /// create a new preicate with EasyPredicates and LogicalType to judge is it satisfied on self
    ///
    /// - Parameters:
    ///   - predicates: predicates rules
    ///   - logic: predicates relative
    /// - Returns: tuple of result and self
    @discardableResult
    func waitUntil(predicates: [EasyPredicate], logic: NSCompoundPredicate.LogicalType = .and, timeout: TimeInterval = 10, handler: XCTNSPredicateExpectation.Handler? = nil) -> (result: XCTWaiter.Result, element: T) {
        if predicates.count <= 0 { fatalError("predicates cannpt be empty!") }
        
        let test = XCTestCase().then { $0.continueAfterFailure = true }
        let promise = test.expectation(for: predicates.toPredicate(logic), evaluatedWith: self, handler: handler)
        let result = XCTWaiter().wait(for: [promise], timeout: timeout)
        return (result, self)
    }
    
    /// assert by new preicate with EasyPredicates and LogicalType, if assert is passed then return self or return nil
    ///
    /// - Parameters:
    ///   - predicates: rules
    ///   - logic: predicates relative
    /// - Returns: self or nil
    @discardableResult
    func assertBreak(predicates: [EasyPredicate], logic: NSCompoundPredicate.LogicalType = .and) -> T? {
        if predicates.first == nil { fatalError("❌ predicates can't be empty") }
        
        let filteredElements = ([self] as NSArray).filtered(using: predicates.toPredicate(logic))
        if filteredElements.isEmpty {
            let predicateStr = predicates.map { "\n <\($0.rawValue.regularString)>" }.joined()
            assertionFailure("\(self) is not satisfied logic:\(logic) about rules: \(predicateStr)")
        }
        return filteredElements.isEmpty ? nil : self
    }
}
Expand for XCUIElement base extensioin
// MARK: - wait
@discardableResult
func waitUntil(predicate: EasyPredicate, timeout: TimeInterval = 10, handler: XCTNSPredicateExpectation.Handler? = nil) -> (result: XCTWaiter.Result, element: XCUIElement) {}

@discardableResult
func waitUntilExists(timeout: TimeInterval = 10) -> (result: XCTWaiter.Result, element: XCUIElement) {}

@discardableResult
func wait(_ s: UInt32 = 1) -> XCUIElement {}

// MARK: - assert
@discardableResult
func assertBreak(predicate: EasyPredicate) -> XCUIElement? {}

@discardableResult
func assert(predicate: EasyPredicate) -> XCUIElement {}

@discardableResult
func waitUntilExistsAssert(timeout: TimeInterval = 10) -> XCUIElement {}

@discardableResult
func assert(predicate: EasyPredicate, timeout: TimeInterval = 10) -> XCUIElement {}
Expand for XCUIElement custom extensioin
// MARK: - Extension
public extension XCUIElement {
    
    /// get the results in the descendants which matching the EasyPredicates
    ///
    /// - Parameters:
    ///   - predicates: EasyPredicate's rules
    ///   - logic: rule's relate
    /// - Returns: result target
    @discardableResult
    func descendants(predicates: [EasyPredicate], logic: NSCompoundPredicate.LogicalType = .and) -> XCUIElementQuery {}
    @discardableResult
    func descendants(predicate: EasyPredicate) -> XCUIElementQuery {}
    
    /// Returns a query for direct children of the element matching with EasyPredicates
    ///
    /// - Parameters:
    ///   - predicates: EasyPredicate rules
    ///   - logic: rules relate
    /// - Returns: result query
    @discardableResult
    func children(predicates: [EasyPredicate], logic: NSCompoundPredicate.LogicalType = .and) -> XCUIElementQuery {}
    @discardableResult
    func children(predicate: EasyPredicate) -> XCUIElementQuery {}
    
    /// Wait until it's available and then type a text into it.
    @discardableResult
    func tapAndType(text: String, timeout: TimeInterval = 10) -> XCUIElement {}
    
    /// Wait until it's available and clear the text, then type a text into it.
    @discardableResult
    func clearAndType(text: String, timeout: TimeInterval = 10) -> XCUIElement {}
    
    @discardableResult
    func hidenKeyboard(inApp: XCUIApplication) -> XCUIElement {}
    
    @discardableResult
    func setSwitch(on: Bool, timeout: TimeInterval = 10) -> XCUIElement  {}
    
    @discardableResult
    func forceTap(timeout: TimeInterval = 10) -> XCUIElement {}
    
    @discardableResult
    func tapIfExists(timeout: TimeInterval = 10) -> XCUIElement {}
}
Expand for Sequence: XCUIElement extension
extension Sequence where Element: XCUIElement {
    
    /// get the elements which match with identifiers and predicates limited in timeout
    ///
    /// - Parameters:
    ///   - predicates: predicates as the match rules
    ///   - logic: relation of predicates
    ///   - timeout: if timeout == 0, return the elements immediately otherwise retry until timeout
    /// - Returns: get the elements
    func elements(predicates: [EasyPredicate], logic: NSCompoundPredicate.LogicalType, timeout: Int) -> [Element] {}
    
    /// get the first element was matched predicate
    func anyElement(predicate: EasyPredicate) -> Element? {}
}

3.4 extension XCUIElementQuery

Expand for XCUIElementQuery extension
public extension XCUIElementQuery {
    /// safe to get index
    ///
    /// - Parameter index: index
    /// - Returns: optional element
    func element(safeIndex index: Int) -> XCUIElement? {    }
    
    /// asset empty of query
    ///
    /// - Parameter empty: bool value
    /// - Returns: optional query self
    func assertEmpty(empty: Bool = false) -> XCUIElementQuery? {    }

    /// get the results which matching the EasyPredicates
    ///
    /// - Parameters:
    ///   - predicates: EasyPredicate's rules
    ///   - logic: rules relate
    /// - Returns: ElementQuery
    func matching(predicates: [EasyPredicate], logic: NSCompoundPredicate.LogicalType = .and) -> XCUIElementQuery {    }
    func matching(predicate: EasyPredicate) -> XCUIElementQuery {    }
    
    /// get the taget element which matching the EasyPredicates
    ///
    /// - Parameters:
    ///   - predicates: EasyPredicate's rules
    ///   - logic: rule's relate
    /// - Returns: result target
    func element(predicates: [EasyPredicate], logic: NSCompoundPredicate.LogicalType = .and) -> XCUIElement {    }
    func element(predicate: EasyPredicate) -> XCUIElement {    }

    /// get the results in the query's descendants which matching the EasyPredicates
    ///
    /// - Parameters:
    ///   - predicates: EasyPredicate's rules
    ///   - logic: rule's relate
    /// - Returns: result target
    func descendants(predicates: [EasyPredicate], logic: NSCompoundPredicate.LogicalType = .and) -> XCUIElementQuery {    }
    func descendants(predicate: EasyPredicate) -> XCUIElementQuery {    }

    /// filter the query by rules to create new query
    ///
    /// - Parameters:
    ///   - predicates: EasyPredicate's rules
    ///   - logic: rule's relate
    /// - Returns: result target
    func containing(predicates: [EasyPredicate], logic: NSCompoundPredicate.LogicalType = .and) -> XCUIElementQuery {    }
    func containing(predicate: EasyPredicate) -> XCUIElementQuery {    }
}

3.5 extension XCTestCase

Expand for XCTestCase (runtime)
/**
 associated object
 */
public extension XCTestCase {
    private struct XCTestCaseAssociatedKey { 
    	static var app = 0 
    }
    var app: XCUIApplication {
        set {
            objc_setAssociatedObject(self, &XCTestCaseAssociatedKey.app, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
        }
        get {
            let _app = objc_getAssociatedObject(self, &XCTestCaseAssociatedKey.app) as? XCUIApplication
            guard let app = _app else { return XCUIApplication().then { self.app = $0 } }
            return app
        }
    }
}
Expand for XCTestCase extension
public extension XCTestCase {
    
    // MARK: - methods
    func isSimulator() -> Bool {}
    func takeScreenshot(activity: XCTActivity, name: String = "Screenshot") {}
    func takeScreenshot(groupName: String = "--- Screenshot ---", name: String = "Screenshot") {}
    func group(text: String = "Group", closure: (_ activity: XCTActivity) -> ()) {}
    func hideAlertsIfNeeded() {}
    func setAirplane(_ value: Bool) {}
    func deleteMyAppIfNeed() {}
    
    /// Try to force launch the application. This structure tries to ovecome the issues described at https://forums.developer.apple.com/thread/15780
    func tryLaunch<T: RawRepresentable>(arguments: [T], count counter: Int = 10, wait: UInt32 = 2) where T.RawValue == String {}
    
    func tryLaunch(count counter: Int = 10) {}
    
    func killAppAndRelaunch() {}
    
    /// Try to force closing the application
    func tryTearDown(wait: UInt32 = 2) {}
}

Author

XcodeYang, xcodeyang@gmail.com

License

Einstein is available under the MIT license. See the LICENSE file for more info.