Skip to content

Instantly share code, notes, and snippets.

@AvdLee
Last active June 7, 2023 07:28
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save AvdLee/719b2de80d74fc503ca1c64a23706d93 to your computer and use it in GitHub Desktop.
Save AvdLee/719b2de80d74fc503ca1c64a23706d93 to your computer and use it in GitHub Desktop.
This extension should make it fairly easy to test your Share Extension. Read more about it here: https://www.avanderlee.com/swift/ui-test-share-extension/
//
// 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
Copy link

caodoan commented Jan 6, 2020

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]

@AvdLee
Copy link
Author

AvdLee commented Jan 7, 2020

@caodoan that's too bad! Any idea how to fix it? I can update the code with that!

@caodoan
Copy link

caodoan commented Jan 7, 2020

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.

@AvdLee
Copy link
Author

AvdLee commented Jan 8, 2020

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!

@el-hoshino
Copy link

Facing exactly the same problem now. And currently all I figured out is:

  1. You can get the whole activity view controller by let shareList = app.otherElements["ActivityListView"]
  2. 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)
    }
}

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