deep-linking-sample/Apps/Wikipedia/WikipediaUITests/XCUIApplication+SnapshotUtilities.swift
Javier Cicchelli 9bcdaa697b [Setup] Basic project structure (#1)
This PR contains all the work related to setting up this project as required to implement the [Assignment](https://repo.rock-n-code.com/rock-n-code/deep-linking-assignment/wiki/Assignment) on top, as intended.

To summarise this work:
- [x] created a new **Xcode** project;
- [x] cloned the `Wikipedia` app and inserted it into the **Xcode** project;
- [x] created the `Locations` app and also, its `Libraries` package;
- [x] created the `Shared` package to share dependencies between the apps;
- [x] added a `Makefile` file and implemented some **environment** and **help** commands.

Co-authored-by: Javier Cicchelli <javier@rock-n-code.com>
Reviewed-on: rock-n-code/deep-linking-assignment#1
2023-04-08 18:37:13 +00:00

196 lines
9.4 KiB
Swift

import XCTest
import WMF
let pressDuration = 0.1 // don't set this to too large a value or presses which happen to land on buttons will activate the button instead of letting the drag event bubble up
extension XCUIElement {
@discardableResult func wmf_tap() -> Bool {
guard exists else { return false }
if isHittable {
tap()
} else {
coordinate(withNormalizedOffset: CGVector(dx: 0.0, dy: 0.0)).tap()
}
return true
}
func wmf_typeText(text: String) -> Bool {
guard exists else { return false }
typeText(text)
return true
}
func wmf_waitUntilExists(timeout: TimeInterval = 10) -> XCUIElement? {
if exists && isHittable {
return self
}
let result = XCTWaiter.wait(for: [XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == true AND isHittable = true"), object: self)], timeout: timeout)
if result != .completed {
return nil
}
return self
}
func wmf_firstButton(withTranslationIn keys: [String], convertTranslationSubstitutionStringsToWildcards shouldConvert: Bool = false) -> XCUIElement? {
return buttons.wmf_firstElement(with: .label, withTranslationIn: keys, convertTranslationSubstitutionStringsToWildcards: shouldConvert)
}
@discardableResult func wmf_tapFirstButton(withTranslationIn keys: [String], convertTranslationSubstitutionStringsToWildcards shouldConvert: Bool = false) -> Bool {
if let firstButton = wmf_firstButton(withTranslationIn: keys, convertTranslationSubstitutionStringsToWildcards: shouldConvert) {
return firstButton.wmf_tap()
} else {
return false
}
}
func wmf_firstStaticText(withTranslationIn keys: [String], convertTranslationSubstitutionStringsToWildcards shouldConvert: Bool = false) -> XCUIElement? {
return staticTexts.wmf_firstElement(with: .label, withTranslationIn: keys, convertTranslationSubstitutionStringsToWildcards: shouldConvert)
}
@discardableResult func wmf_tapFirstStaticText(withTranslationIn keys: [String], convertTranslationSubstitutionStringsToWildcards shouldConvert: Bool = false) -> Bool {
if let firstStaticText = wmf_firstStaticText(withTranslationIn: keys, convertTranslationSubstitutionStringsToWildcards: shouldConvert) {
return firstStaticText.wmf_tap()
} else {
return false
}
}
func wmf_firstSwitch(withTranslationIn keys: [String], convertTranslationSubstitutionStringsToWildcards shouldConvert: Bool = false) -> XCUIElement? {
return switches.wmf_firstElement(with: .label, withTranslationIn: keys, convertTranslationSubstitutionStringsToWildcards: shouldConvert)
}
@discardableResult func wmf_tapFirstSwitch(withTranslationIn keys: [String], convertTranslationSubstitutionStringsToWildcards shouldConvert: Bool = false) -> Bool {
if let firstSwitch = wmf_firstSwitch(withTranslationIn: keys, convertTranslationSubstitutionStringsToWildcards: shouldConvert) {
return firstSwitch.wmf_tap()
} else {
return false
}
}
@discardableResult func wmf_firstSearchField(withTranslationIn keys: [String], convertTranslationSubstitutionStringsToWildcards shouldConvert: Bool = false) -> XCUIElement? {
return searchFields.wmf_firstElement(with: .placeholderValue, withTranslationIn: keys, convertTranslationSubstitutionStringsToWildcards: shouldConvert)
}
@discardableResult func wmf_tapFirstCloseButton() -> Bool {
return wmf_tapFirstButton(withTranslationIn: ["close-button-accessibility-label"])
}
@discardableResult func wmf_tapFirstNavigationBarBackButton() -> Bool {
let backButtonTapped = wmf_tapFirstButton(withTranslationIn: ["back", "back-button-accessibility-label", "home-title"])
guard !backButtonTapped else {
return backButtonTapped
}
// Needed because if the title is long, the back button sometimes won't have text, as seen on https://stackoverflow.com/q/38595242/135557
if let button = navigationBars.buttons.element(boundBy: 0).wmf_waitUntilExists() {
return button.wmf_tap()
} else {
return false
}
}
@discardableResult func wmf_tapFirstCollectionViewCell() -> Bool {
if let cell = collectionViews.children(matching: .cell).element(boundBy: 0).wmf_waitUntilExists() {
return cell.wmf_tap()
} else {
return false
}
}
@discardableResult func wmf_tapFirstTableViewCell() -> Bool {
if let cell = tables.children(matching: .cell).element(boundBy: 0).wmf_waitUntilExists() {
return cell.wmf_tap()
} else {
return false
}
}
@discardableResult func wmf_scrollToTop() -> Bool {
if let statusBar = statusBars.element(boundBy: 0).wmf_waitUntilExists() {
let tapResult = statusBar.wmf_tap()
return tapResult
} else {
return false
}
}
func wmf_scrollElementToTop(element: XCUIElement, yOffset: CGFloat = 0.0) {
let normalizedOffset = CGVector(
dx: 0.5,
dy: 0.0 /* 0.0 is important - in case only top of view is above bottom of screen! (if we were scrolling elements to bottom of screen this would need to be 1.0) */
)
let elementTopCoord = element.coordinate(withNormalizedOffset: normalizedOffset)
elementTopCoord.press(forDuration: pressDuration, thenDragTo: coordinate(withNormalizedOffset: CGVector(dx: 0, dy: yOffset)))
}
func wmf_scrollDown(times: Int = 1, dragStartY: Double = 0.8) {
for _ in 0 ..< times {
// If dragStartY set to 1.0 it drags from very bottom of the screen which triggers an iPad task switcher and zooms out the app. So drag from a little above the very bottom - hence the 0.8 dragStartY default value.
coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: dragStartY)).press(forDuration: pressDuration, thenDragTo: coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.0)))
}
}
// Scrolls to first element for each ScrollItem key in single scrolling pass (i.e. without scrolling back to top between items).
func wmf_scrollToFirstElements(matching type: XCUIElement.ElementType, yOffset: CGFloat, items: [ScrollItem], timeout seconds: Double = 180) {
let start = Date()
var keys = items.map {$0.key}
scrollLoop: repeat {
if let element = descendants(matching: type).wmf_firstElement(with: .label, withTranslationIn: keys, convertTranslationSubstitutionStringsToWildcards: true, timeout: 1) {
if let item = items.first(where: {$0.predicate.evaluate(with: element.label)}) {
wmf_scrollElementToTop(element: element, yOffset: yOffset)
item.success(element)
if let index = keys.firstIndex(of: item.key) {
keys.remove(at: index)
}
continue scrollLoop // Need to skip `wmf_scrollDown()` because other elements may already be onscreen and we don't want to scroll any of them offscreen. This lets the next pass(es) through the loop catch 'em.
}
}
wmf_scrollDown()
} while (Date().timeIntervalSince(start) < seconds) && (!keys.isEmpty)
}
}
private enum ElementPropertyType: String {
case label
case placeholderValue
case `self`
func predicate(for text: String) -> NSPredicate {
return NSPredicate(format: "\(rawValue) ==[cd] %@", text)
}
func wildcardPredicate(for text: String) -> NSPredicate {
var mutableText = text
for i in 0...9 {
mutableText = mutableText.replacingOccurrences(of: "%\(i)$@", with: "*")
}
mutableText = "*\(mutableText)*"
return NSPredicate(format: "\(rawValue) like[cd] %@", mutableText)
}
func predicate(for texts: [String]) -> NSPredicate {
return NSCompoundPredicate(orPredicateWithSubpredicates: texts.map {text in predicate(for: text)})
}
func wildcardPredicate(for texts: [String]) -> NSPredicate {
return NSCompoundPredicate(orPredicateWithSubpredicates: texts.map {text in wildcardPredicate(for: text)})
}
}
private extension XCUIElementQuery {
func wmf_firstElement(with propertyType: ElementPropertyType, withTranslationIn keys: [String], convertTranslationSubstitutionStringsToWildcards shouldConvert: Bool = false, timeout: TimeInterval = 10) -> XCUIElement? {
let translations = keys.map {key in WMFLocalizedString(key, value: "", comment: "")} // localization strings are copied into this scheme during a build phase: https://stackoverflow.com/a/38133902/135557
let predicate = shouldConvert ? propertyType.wildcardPredicate(for: translations) : propertyType.predicate(for: translations)
return matching(predicate).element(boundBy: 0).wmf_waitUntilExists(timeout: timeout)
}
}
struct ScrollItem {
let key: String
let success: (XCUIElement) -> Void
let predicate: NSPredicate
init(key: String, success: @escaping (XCUIElement) -> Void) {
self.key = key
self.success = success
self.predicate = ElementPropertyType.`self`.wildcardPredicate(for: WMFLocalizedString(key, value: "", comment: ""))
}
}
extension XCUIApplication {
func dismissPopover() {
otherElements["PopoverDismissRegion"].tap()
}
}