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 protocolUIAccessibilityIdentification
and all enum's rawValue is default to followRawRepresentable
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 isPredicateRawValue
(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.