Skip to content

Instantly share code, notes, and snippets.

@pilot34
Last active March 26, 2020 21:53
Show Gist options
  • Save pilot34/09d692f74d4052670f3bae77dd745889 to your computer and use it in GitHub Desktop.
Save pilot34/09d692f74d4052670f3bae77dd745889 to your computer and use it in GitHub Desktop.
//
// UITestCase.swift
//
// Created by Gleb Tarasov on 23/04/2018.
//
import Foundation
import XCTest
import UIKit
class UITestCase: XCTestCase {
private func wait(forElement element: XCUIElement, exists: Bool, timeout: TimeInterval) {
let predicate = NSPredicate(format: "exists == %@", NSNumber(value: exists))
let e = XCTNSPredicateExpectation(predicate: predicate, object: element)
let result = XCTWaiter().wait(for: [ e ], timeout: timeout)
XCTAssert(result == .completed)
}
func wait(forWebViewElement element: XCUIElementTypeQueryProvider, timeout: TimeInterval = 20) {
// xcode has bug, so we cannot directly access webViews XCUIElements
// as a workaround we can check debugDesciption and parse it, that works
let predicate = NSPredicate { obj, _ in
guard let el = obj as? XCUIElement else {
return false
}
// If element has firstMatch, than there will be description of that at the end
// If no match - it will be ended with "FirstMatch\n"
return !el.firstMatch.debugDescription.hasSuffix("First Match\n")
}
// we need to take .firstMatch, because we parse description for that
let e = XCTNSPredicateExpectation(predicate: predicate, object: element.firstMatch)
let result = XCTWaiter().wait(for: [ e ], timeout: timeout)
XCTAssert(result == .completed)
}
func wait(forElement element: XCUIElement, timeout: TimeInterval = 20) {
wait(forElement: element, exists: true, timeout: timeout)
}
func wait(elementToHide element: XCUIElement, timeout: TimeInterval = 20) {
wait(forElement: element, exists: false, timeout: timeout)
}
func wait(seconds: TimeInterval) {
Thread.sleep(forTimeInterval: seconds)
}
private func coordinate(forWebViewElement element: XCUIElement) -> XCUICoordinate? {
// parse description to find its frame
let descr = element.firstMatch.debugDescription
guard let rangeOpen = descr.range(of: "{{", options: [.backwards]),
let rangeClose = descr.range(of: "}}", options: [.backwards]) else {
return nil
}
let frameStr = String(descr[rangeOpen.lowerBound..<rangeClose.upperBound])
let rect = CGRectFromString(frameStr)
// tap on the center
let center = CGVector(dx: rect.midX, dy: rect.midY)
let coordinate = XCUIApplication().coordinate(withNormalizedOffset: .zero).withOffset(center)
return coordinate
}
func tap(onWebViewElement element: XCUIElement) {
// xcode has bug, so we cannot directly access webViews XCUIElements
// as workaround we can check debugDesciption, find frame and tap by coordinate
// wait for element to appear before tap
wait(forWebViewElement: element)
let coord = coordinate(forWebViewElement: element)
coord?.tap()
}
func exists(webViewElement element: XCUIElement) -> Bool {
return coordinate(forWebViewElement: element) != nil
}
func typeText(_ text: String, toWebViewField element: XCUIElement) {
// xcode has bug, so we cannot directly access webViews XCUIElements
// as workaround we can check debugDesciption, find frame, tap by coordinate,
// and then paste text there
// wait for element to appear before tap
wait(forWebViewElement: element)
guard let coordBeforeTap = coordinate(forWebViewElement: element) else {
XCTFail("no element \(element)")
return
}
// "typeText" doesn't work, so we paste text
// first tap to activate field
UIPasteboard.general.string = text
coordBeforeTap.tap()
// wait for keyboard to appear
wait(forWebViewElement: XCUIApplication().keyboards.firstMatch)
// after tap coordinate can change
guard let coordAfterTap = coordinate(forWebViewElement: element) else {
XCTFail("no element \(element)")
return
}
// tap one more time for "paste" menu
coordAfterTap.press(forDuration: 1)
wait(forElement: XCUIApplication().menuItems["Paste"])
if XCUIApplication().menuItems["Select All"].exists {
// if there was a text - remove it, by pressing Select All and Cut
XCUIApplication().menuItems["Select All"].tap()
XCUIApplication().menuItems["Cut"].tap()
// close keyboard
XCUIApplication().toolbars.buttons["Done"].tap()
// call this method one more time
typeText(text, toWebViewField: element)
return
}
XCUIApplication().menuItems["Paste"].tap()
// close keyboard
XCUIApplication().toolbars.buttons["Done"].tap()
}
}
@seyitcodeit
Copy link

Hi Gleb i am working for same issue. Could you please share with me a sample test class for use these function to test ?

@pilot34
Copy link
Author

pilot34 commented Mar 26, 2020

@seyitcodeit
check disclaimer for this article: https://medium.com/@pilot34/work-with-sfsafariviewcontroller-or-wkwebview-in-xcode-ui-tests-8b14fd281a1f
we don't need this code now, there is an easier workaround

@seyitcodeit
Copy link

Thank you for your quick reply and article actually( this is the one of a few articles about this issue) i am trying to reference this article but i am little confused. Should i use this functions to test my webview login screen ? Since i have just coordinates from debugdescription of my textfields no placeholders.
let textfield = app.textFields["username"]
typeText("[email protected]", toWebViewField: textfield)

it fails because dont find username so only solution remains coordinate ? Thank you

@pilot34
Copy link
Author

pilot34 commented Mar 26, 2020

I'm not in the context, this was 2 years ago. But as I remember, you should be able to enter the text into the text field inside WKWebView just with XCUIElement default method:

app.textFields["username"].typeText("[email protected]")

So the hack in this Gist is not needed at all. But maybe I'm wrong.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment