Coder Social home page Coder Social logo

zhipingyang / einstein Goto Github PK

View Code? Open in Web Editor NEW
16.0 1.0 2.0 378 KB

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.

Home Page: https://zhipingyang.github.io/Einstein/

License: MIT License

Swift 94.33% Ruby 4.57% Objective-C 0.90% Shell 0.19%
uitest uitesting uikit-accessibilityidentifier xctest xcuielement ui-testing xcuitest swift5 swift rawrepresentable-extension

einstein's Introduction


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, [email protected]

License

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

einstein's People

Contributors

zhipingyang avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

einstein's Issues

combine enum's the RawValue type define and it's protocol as the one

public protocol PrettyRawRepresentable: RawRepresentable where RawValue == String {
    var prettyRawValue: RawValue { get }
}
public extension PrettyRawRepresentable {
    var prettyRawValue: String {
        let paths = String(reflecting: self).split(separator: ".").dropFirst()
        if String(paths.last ?? "") != rawValue {
            return rawValue
        }
        return paths.joined(separator: "_")
    }
}

As we already defined PrettyRawRepresentable's RawValue == String,
but enum cannot write as blow, why?

enum Home: PrettyRawRepresentable {    
    case setting
} 
let str = Home.seeting.prettyRawValue // Home_setting

current

enum Home: String, PrettyRawRepresentable {
    case setting
}

let str = Home.seeting.prettyRawValue // Home_setting

wants

enum Home: XXX {
    case setting
}
let str = Home.seeting.prettyRawValue // Home_setting
  • XXX can be a protocol or a struct, but should better as a subprotocol of RawRepresentable
  • XXX method already be achieved (no more code as the blow)

see more: AccessibilityIdentifier

About the `EasyPredicate` design

PredicateRawValue is RawValue type of EasyPredicate

current

public enum PredicateRawValue: RawRepresentable {
    public var rawValue: String { return xxx }
    public init?(rawValue: String) {
        self = .custom(regular: rawValue)
    }
    case bool(key: PredicateKey.bool, comparison: Comparison, value: Bool)
    case string(key: PredicateKey.string, comparison: Comparison, value: String)
    case type(value: XCUIElement.ElementType)
    case custom(regular: String)
}

the thinking of new design

enum PredicateRawValue<Key: RawRepresentable, Value: RawRepresentable>: RawRepresentable where Key.RawValue == String {
    var rawValue: String { return xxx }
    init?(rawValue: String) {
        self = .custom(regular: rawValue)
    }
    case keyValue(key: Key, comparison: Comparison, value: Value)
    case custom(regular: String) // unrelated with Key & Value define, shit ๐Ÿ’ฉ
}

How combine two functions as the one in `AccessibilityIdentifier`

๐Ÿ˜ค

Here is two extensions PrettyRawRepresentable+helpers.swift and RawRepresentable+helpers.swift which are support different situation on enum.
But the most unacceptable thing is that their API is almost the same

enum WayOne: String, PrettyRawRepresentable {
    case theWay
}
enum WayTwo: String {
    case theWay
}
wayOneView >>> WayOne.theWay
wayTwoView >>> WayTwo.theWay

// wayOneView.accessibilityIdentifier == "WayOne_theWay"
// called in PrettyRawRepresentable+helpers.swift
let element_1: XCUIElement = WayOne.theWay.element 

// wayTwoView.accessibilityIdentifier == "theWay"
// called in RawRepresentable+helpers.swift
let element_2: XCUIElement = WayTwo.theWay.element

current

public protocol PrettyRawRepresentable: RawRepresentable where RawValue == String {
    var prettyRawValue: RawValue { get }
}

public extension PrettyRawRepresentable {
    var prettyRawValue: String {
        let paths = String(reflecting: self).split(separator: ".").dropFirst()
        if String(paths.last ?? "") != rawValue {
            return rawValue
        }
        return paths.joined(separator: "_")
    }
}

infix operator >>>
public func >>> <T: RawRepresentable>(lhs: UIAccessibilityIdentification?, rhs: T) where T.RawValue == String {
    lhs?.accessibilityIdentifier = rhs.rawValue
}
public func >>> <T: PrettyRawRepresentable>(lhs: UIAccessibilityIdentification?, rhs: T) {
    lhs?.accessibilityIdentifier = rhs.prettyRawValue
}

wants the design

only use one function

public protocol PrettyRawRepresentable: RawRepresentable where RawValue == String {
    var rawValue: RawValue { get }
}

public extension PrettyRawRepresentable {
    var rawValue: String {
        // how todo
    }
}

infix operator >>>
public func >>> <T: RawRepresentable>(lhs: UIAccessibilityIdentification?, rhs: T) where T.RawValue == String {
    lhs?.accessibilityIdentifier = rhs.rawValue
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.