项目作者: ZhipingYang

项目描述 :
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.
高级语言: Swift
项目地址: git://github.com/ZhipingYang/Einstein.git
创建时间: 2019-07-31T01:24:57Z
项目社区:https://github.com/ZhipingYang/Einstein

开源协议:MIT License

下载







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 ↓

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

😵 without Einstein ↓

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

File structures

  1. ─┬─ Einstein
  2. ├─┬─ Identifier: -> `UIKit`
  3. └─── AccessibilityIdentifier.swift
  4. └─┬─ UITest: -> `Einstein/Identifier` & `XCTest` & `Then`
  5. ├─┬─ Model
  6. ├─── EasyPredicate.swift
  7. └─── Springboard.swift
  8. └─┬─ Extensions
  9. ├─── RawRepresentable+helpers.swift
  10. ├─── PrettyRawRepresentable+helpers.swift
  11. ├─── XCTestCase+helpers.swift
  12. ├─── XCUIElement+helpers.swift
  13. └─── XCUIElementQuery+helpers.swift

Install

required iOS >= 9.0 Swift5.0 with Cocoapods

  1. target 'XXXProject' do
  2. # in project target
  3. pod 'Einstein/Identifier'
  4. target 'XXXProjectUITests' do
  5. # in UITest target
  6. pod 'Einstein'
  7. end
  8. 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

swift 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.

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

swift // 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
>
> swift > @testable import XXXPreject >

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



swift 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

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

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

swift /* 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

swift 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

swift /* 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)

swift 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

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

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

swift 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

swift 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)

swift /** 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

swift 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.