-
-
Save AvdLee/719b2de80d74fc503ca1c64a23706d93 to your computer and use it in GitHub Desktop.
// | |
// XCTestCaseExtensions.swift | |
// | |
// Useful extension to UI Test the Share Extension of apps. | |
// | |
// | |
// Created by Antoine van der Lee on 18/05/2018. | |
// Copyright © 2018. All rights reserved. | |
// | |
import XCTest | |
extension XCTestCase { | |
/// Opens the Safari App for UI Testing with the given URL. | |
/// | |
/// - Parameter url: The URL to open. | |
/// - Returns: The `XCUIApplication` pointing to the Safari App. | |
func openSafari(with url: URL) -> XCUIApplication { | |
let safariApp: XCUIApplication = XCUIApplication(bundleIdentifier: "com.apple.mobilesafari") | |
// Start the Safari app as we're starting test from there. | |
safariApp.launch() | |
// Wait for Safari to be the active application. | |
let safariAppActiveExpectation = XCTNSPredicateExpectation(predicate: NSPredicate(format: "state == \(XCUIApplication.State.runningForeground.rawValue)"), object: safariApp) | |
wait(for: [safariAppActiveExpectation], timeout: 10) | |
// Tap the domain search field and enter the url. | |
let urlBar = safariApp.otherElements["URL"] | |
XCTAssert(urlBar.waitForExistence(timeout: 3.0), "The URL Bar should exist after opening Safari.") | |
urlBar.tap() | |
let urlTextField = safariApp.textFields["URL"] | |
XCTAssert(urlTextField.waitForExistence(timeout: 3), "Even with large pages with expect the urlTextField to be available after 3 seconds") | |
urlTextField.typeText(url.absoluteString) | |
safariApp.keyboards.firstMatch.buttons["Go"].tap() | |
return safariApp | |
} | |
/// Opens the share extension for the given name. | |
/// | |
/// - Parameters: | |
/// - name: The name of the Share Extension to open. | |
/// - application: The application in which we're trying to open the share extension. This application must contain a share extension button. | |
func openShareExtension(name: String, in application: XCUIApplication) { | |
// Open the share sheet | |
let shareButton = application.toolbars.buttons["Share"] | |
let hittableExpectation = expectation(for: NSPredicate(format: "hittable == 1"), evaluatedWith: shareButton, handler: nil) | |
wait(for: [hittableExpectation], timeout: 5) | |
shareButton.tap() | |
let shareExtensionButton = application.collectionViews.collectionViews.buttons[name] | |
if !shareExtensionButton.waitForExistence(timeout: 3) { | |
// Check if our Share Extension is available and enable it when needed. | |
let moreButton = application.collectionViews.collectionViews.buttons["More"] | |
application.collectionViews.collectionViews.element(boundBy: 1).scrollToElement(element: moreButton, in: application) | |
XCTAssert(moreButton.waitForExistence(timeout: 10), "More button not found") | |
moreButton.tap() | |
// Find the share extension switch. | |
let shareExtensionSwitch = application.tables.switches[name].firstMatch | |
application.tables.element(boundBy: 0).scrollToElement(element: shareExtensionSwitch, in: application) | |
XCTAssert(shareExtensionSwitch.waitForExistence(timeout: 10), "Share Extension Switch not found") | |
// Only tap the extension switch if it's off. | |
if shareExtensionSwitch.value as? String != "1" { | |
shareExtensionSwitch.tap() | |
} | |
// Close the switches view. | |
let activityDoneButton = application.navigationBars["Activities"].buttons["Done"].firstMatch | |
XCTAssert(activityDoneButton.waitForExistence(timeout: 10), "Done button not found") | |
activityDoneButton.tap() | |
} | |
// Open the share extension. | |
application.collectionViews.collectionViews.element(boundBy: 1).scrollToElement(element: shareExtensionButton, in: application, direction: .reverse) | |
XCTAssert(shareExtensionButton.waitForExistence(timeout: 10), "Share extension button not found") | |
shareExtensionButton.tap() | |
} | |
} | |
private extension XCUIElement { | |
enum HorizontalScrollDirection { | |
case reverse | |
case forward | |
var offset: CGVector { | |
switch self { | |
case .reverse: | |
return CGVector(dx: 100, dy: 100) | |
case .forward: | |
return CGVector(dx: -100, dy: -100) | |
} | |
} | |
} | |
/// Scrolls the given application in the given direction until the given element is visible. | |
/// Pass any other elements which shouldn't intersect. This can be useful if you have floating buttons which could exist on top of the element we're looking for. If the element would intersect, tapping it would instead tap the overlaying button. | |
/// | |
/// - Parameters: | |
/// - element: The element we're looking for. | |
/// - application: The application in which we're searching. | |
/// - direction: The direction to scroll to. Defaults to `forward`. | |
/// - notIntersectingElements: Any elements which the element we're looking for should not intersect with. | |
func scrollToElement(element: XCUIElement, in application: XCUIApplication = XCUIApplication(), direction: HorizontalScrollDirection = .forward, notIntersecting notIntersectingElements: XCUIElement...) { | |
let notIntersectingFrames = notIntersectingElements.filter { $0.visible() && !$0.frame.isEmpty }.map { $0.frame } | |
while !element.visible(in: application) || notIntersectingFrames.first(where: { element.frame.intersects($0) }) != nil { | |
// We don't use swipeUp to swipe in smaller portions. | |
let start = coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)) | |
let finish = start.withOffset(direction.offset) | |
start.press(forDuration: 0.0, thenDragTo: finish) | |
} | |
} | |
/// Determines whether the element is visible in the bounds of the given application. | |
/// - Returns: `true` if visible, otherwise `false`. | |
func visible(in application: XCUIApplication = XCUIApplication()) -> Bool { | |
guard exists && !frame.isEmpty else { return false } | |
return application.windows.element(boundBy: 0).frame.contains(frame) && isHittable | |
} | |
} |
@caodoan that's too bad! Any idea how to fix it? I can update the code with that!
I tried but was unable to find any identifiable properties from the elements. They are no longer buttons but table cells without any static texts associated. They don't have accessibility identifier and have the same label ('Activity')... If this used to work for you, I guess it's just a system bug introduced in recent iOS versions.
If this used to work for you, I guess it's just a system bug introduced in recent iOS versions.
Most likely, indeed, as it did work before. We're no longer using this test ourselves in production so I'm kind of unable to verify right now. If anyone visits this thread and likes to add his solution, let us know!
Facing exactly the same problem now. And currently all I figured out is:
- You can get the whole activity view controller by
let shareList = app.otherElements["ActivityListView"]
- Extension buttons are now cells so you can find them by
shareList.cells
But then a big problem comes out. For some reason, Messages
and News
cells can be specified with shareList.cells["Messages"]
and shareList.cells["News"]
, but Reminders
, More
as well as my app are all named Activity
, which made it impossible to properly specify the cell you want.
And of course I've also tried to specify the cell by searching its children with shareList.cells.children(matching: .any).allElementsBoundByIndex
, but guess what? There's no child element iterated at all😇
BTW, the versions are:
OS: macOS 10.15.2 (19C57)
Xcode: 11.3 (11C29)
Simulator: iPhone 11 - 13.3
And how I specified the whole activity view controller was by checking all elements' screenshots with:
app.otherElements.allElementsBoundByIndex.forEach { (element) in
if element.exists, element.frame != .zero {
let screenshot = element.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
add(attachment)
}
}
Thanks for sharing! It appears the following logic no longer works though. Extensions don't seem to be inspectable by names anymore...
let shareExtensionButton = application.collectionViews.collectionViews.buttons[name]