爱因斯坦 0.4.0

爱因斯坦 0.4.0

XcodeYang 维护。



  • 作者:
  • Daniel Yang


Documentation Version CI Status License Platform CI Status

爱因斯坦 是一个 UITest 框架,它通过 AccessibilityIdentifier 在整个项目与 UITest 之间集成业务逻辑。在 UITest 上,使用 EasyPredict扩展 以更好地支持 UITest 代码编写。

比较示例

XCTestCase 中输入电话号码进行登录

👍使用 Einstein ↓

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

😵不使用 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")
}

文件结构

─┬─ 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

安装

需要 iOS >= 9.0Swift5.0,并使用 Cocoapods

target 'XXXProject' do

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

使用

  • AccessibilityIdentifier
    • 项目目标
    • UITest 目标
    • 在 UITest 中应用
  • EasyPredicate
  • 扩展

1. 无障碍标识符

说明
所有UIKit的无障碍标识符都是协议UIAccessibilityIdentification的一个属性,所有枚举的rawValue默认遵循RawRepresentable

展开查看详细信息
  • 1.1 定义枚举
    • 设置String中的rawValue
    • 根据需要添加PrettyRawRepresentable
  • 1.2 使用枚举的rawValue设置UIKit的无障碍标识符
    • 方法1:后缀操作符
    • 方法2:UIAccessibilityIdentification的扩展
  • 1.3 在UITest目标中应用

1.1 定义枚举

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

强烈建议在枚举上添加PrettyRawRepresentable协议,这样你将获得带属性路径的RawValue字符串,以避免在不同页面中无障碍标识符相同。

// 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"

更多信息:PrettyRawRepresentable

1.2 使用枚举的rawValue设置UIKit的无障碍标识符

// system way
signInPhoneTextField.accessibilityIdentifier = "LoginAccessID_SignIn_phoneNumber"

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

print(forgetPhoneTextField.accessibilityIdentifier)
// "LoginAccessID_Forget_phoneNumber"

1.3. 在UITest目标中应用

说明
首先在UITest中导入定义的枚举文件

  • 方法1:在XXXProject和XXXUITest中将它的target membership设置为true
  • 方法2:在UITest中使用@testable导入项目文件(链接:如何设置)
@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

说明
EasyPredicate的RawValue是PredicateRawValue(用于管理逻辑和转换NSPredicate的另一个枚举)。

展开查看EasyPredicate的案例
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)
}

尽管 NSPredicate 功能强大,但开发程序接口不太理想,我们可以尝试将硬编码风格转换为面向对象风格,这正是 EasyPredicate 做的事情。

// 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 合并

// "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 扩展

3.1 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 RawRepresentable 扩展

为元素为 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? {}
}
为 RawRepresentable 扩展扩展
/*
 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 扩展 XCUIElement

为 XCUIElement (基本) 扩展
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
    }
}
为 XCUIElement 基础扩展扩展
// 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 {}
为 XCUIElement 自定义扩展扩展
// 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 {}
}
为序列:XCUIElement 扩展扩展
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 扩展 XCUIElementQuery

为 XCUIElementQuery 扩展扩展
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 扩展 XCTestCase

为 XCTestCase (运行时) 扩展
/**
 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
        }
    }
}
为 XCTestCase 扩展扩展
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) {}
}

作者

XcodeYang, [email protected]

许可证

Einstein项目受MIT许可证许可。有关更多信息,请参阅LICENSE文件。