From b442842f7a3a48937f7a3fb10a8fef35dbb3afd9 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Wed, 3 Jan 2024 14:21:19 +1100 Subject: [PATCH] Reimplement `waitForElementAndTap...` as a typed `XCUIElement` method "Typed" in that it expects an `enum` instead of a `String` to define the expected element state. --- .../Screens/Editor/EditorPostSettings.swift | 2 +- .../Screens/NotificationsScreen.swift | 6 +- .../XCUIApplication+SavePassword.swift | 2 +- .../XCUIElement+TapUntil.swift | 86 +++++++++++++++++++ WordPress/WordPress.xcodeproj/project.pbxproj | 8 +- 5 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 WordPress/UITestsFoundation/XCUIElement+TapUntil.swift diff --git a/WordPress/UITestsFoundation/Screens/Editor/EditorPostSettings.swift b/WordPress/UITestsFoundation/Screens/Editor/EditorPostSettings.swift index 5dea2cff476e..d05a37823b64 100644 --- a/WordPress/UITestsFoundation/Screens/Editor/EditorPostSettings.swift +++ b/WordPress/UITestsFoundation/Screens/Editor/EditorPostSettings.swift @@ -152,7 +152,7 @@ public class EditorPostSettings: ScreenObject { // To ensure that the day tap happens on the correct month let nextMonth = monthLabel.value as! String if nextMonth != currentMonth { - waitForElementAndTap(firstCalendarDayButton, untilConditionOn: firstCalendarDayButton, condition: "selected", errorMessage: "First Day button not selected!") + firstCalendarDayButton.tapUntil(.selected, failureMessage: "First Day button not selected!") } doneButton.tap() diff --git a/WordPress/UITestsFoundation/Screens/NotificationsScreen.swift b/WordPress/UITestsFoundation/Screens/NotificationsScreen.swift index 4eaef56af45c..56ede9a43f83 100644 --- a/WordPress/UITestsFoundation/Screens/NotificationsScreen.swift +++ b/WordPress/UITestsFoundation/Screens/NotificationsScreen.swift @@ -84,7 +84,11 @@ public class NotificationsScreen: ScreenObject { } public func replyToComment(withText text: String) -> Self { - waitForElementAndTap(replyCommentButton, untilConditionOn: replyTextView, condition: "exists", errorMessage: "Reply Text View does not exists!") + replyCommentButton.tapUntil( + element: replyTextView, + matches: .exists, + failureMessage: "Reply Text View does not exists!" + ) replyTextView.typeText(text) replyButton.tap() diff --git a/WordPress/UITestsFoundation/XCUIApplication+SavePassword.swift b/WordPress/UITestsFoundation/XCUIApplication+SavePassword.swift index a6baa687d5fe..3f274ad6b687 100644 --- a/WordPress/UITestsFoundation/XCUIApplication+SavePassword.swift +++ b/WordPress/UITestsFoundation/XCUIApplication+SavePassword.swift @@ -11,7 +11,7 @@ extension XCUIApplication { // There should be no need to wait for this button to exist since it's part of the same // alert where "Save Password" is. let notNowButton = XCUIApplication().buttons["Not Now"] - waitForElementAndTap(notNowButton, untilConditionOn: notNowButton, condition: "dismissed", errorMessage: "Save Password Prompt not dismissed!") + notNowButton.tapUntil(.dismissed, failureMessage: "Save Password Prompt not dismissed!") } } } diff --git a/WordPress/UITestsFoundation/XCUIElement+TapUntil.swift b/WordPress/UITestsFoundation/XCUIElement+TapUntil.swift new file mode 100644 index 000000000000..03472eba237d --- /dev/null +++ b/WordPress/UITestsFoundation/XCUIElement+TapUntil.swift @@ -0,0 +1,86 @@ +import XCTest + +public extension XCUIElement { + + /// Abstraction do describe possible "states" an `XCUIElement` can be in. + /// + /// The goal of this `enum` is to make checking against the possible states a safe operation thanks to the compiler enforcing all and only the states represented by the `enum` `case`s are handled. + enum State { + case exists + case dismissed + case selected + } + + /// Attempt to tap `self` until the given `XCUIElement` is in the given `State` or the `maxRetries` number of retries has been reached. + /// + /// Useful to make tests robusts against UI changes that may have some lag. + func tapUntil( + element: XCUIElement, + matches state: State, + failureMessage: String, + maxRetries: Int = 10, + retryInterval: TimeInterval = 1 + ) { + tapUntil( + Condition(element: element, state: state), + retriedCount: 0, + failureMessage: failureMessage, + maxRetries: maxRetries, + retryInterval: retryInterval + ) + } + + /// Attempt to tap `self` until its "state" matches `Condition.State` or the `maxRetries` number of retries has been reached. + /// + /// Useful to make tests robusts against UI changes that may have some lag. + func tapUntil( + _ state: State, + failureMessage: String, + maxRetries: Int = 10, + retryInterval: TimeInterval = 1 + ) { + tapUntil( + Condition(element: self, state: state), + retriedCount: 0, + failureMessage: failureMessage, + maxRetries: maxRetries, + retryInterval: retryInterval + ) + } + + /// Describe the expectation for a given `XCUIElement` to be in a certain `Condition.State`. + /// + /// Example: `Condition(element: myButton, state: .selected)`. + struct Condition { + + let element: XCUIElement + let state: XCUIElement.State + + fileprivate func isMet() -> Bool { + switch state { + case .exists: return element.exists + case .dismissed: return element.isHittable + case .selected: return element.isSelected + } + } + } + + private func tapUntil( + _ condition: Condition, + retriedCount: Int, + failureMessage: String, + maxRetries: Int, + retryInterval: TimeInterval + ) { + guard retriedCount < maxRetries else { + return XCTFail("\(failureMessage) after \(retriedCount) tries.") + } + + tap() + + guard condition.isMet() else { + sleep(UInt32(retryInterval)) + return tapUntil(condition, retriedCount: retriedCount + 1, failureMessage: failureMessage, maxRetries: maxRetries, retryInterval: retryInterval) + } + } +} diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 70edf8697853..1f6f2b2e5fd1 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -879,6 +879,7 @@ 37022D931981C19000F322B7 /* VerticallyStackedButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 37022D901981BF9200F322B7 /* VerticallyStackedButton.m */; }; 374CB16215B93C0800DD0EBC /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 374CB16115B93C0800DD0EBC /* AudioToolbox.framework */; }; 37EAAF4D1A11799A006D6306 /* CircularImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAAF4C1A11799A006D6306 /* CircularImageView.swift */; }; + 3F03F2BD2B45041E00A9CE99 /* XCUIElement+TapUntil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F03F2BC2B45041E00A9CE99 /* XCUIElement+TapUntil.swift */; }; 3F09CCA82428FF3300D00A8C /* ReaderTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F09CCA72428FF3300D00A8C /* ReaderTabViewController.swift */; }; 3F09CCAA2428FF8300D00A8C /* ReaderTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F09CCA92428FF8300D00A8C /* ReaderTabView.swift */; }; 3F09CCAE24292EFD00D00A8C /* ReaderTabItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F09CCAD24292EFD00D00A8C /* ReaderTabItem.swift */; }; @@ -6583,6 +6584,7 @@ 37EAAF4C1A11799A006D6306 /* CircularImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularImageView.swift; sourceTree = ""; }; 3AB6A3B516053EA8D0BC3B17 /* Pods-JetpackStatsWidgets.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JetpackStatsWidgets.release-alpha.xcconfig"; path = "../Pods/Target Support Files/Pods-JetpackStatsWidgets/Pods-JetpackStatsWidgets.release-alpha.xcconfig"; sourceTree = ""; }; 3C8DE270EF0498A2129349B0 /* Pods-JetpackNotificationServiceExtension.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JetpackNotificationServiceExtension.release-alpha.xcconfig"; path = "../Pods/Target Support Files/Pods-JetpackNotificationServiceExtension/Pods-JetpackNotificationServiceExtension.release-alpha.xcconfig"; sourceTree = ""; }; + 3F03F2BC2B45041E00A9CE99 /* XCUIElement+TapUntil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIElement+TapUntil.swift"; sourceTree = ""; }; 3F09CCA72428FF3300D00A8C /* ReaderTabViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderTabViewController.swift; sourceTree = ""; }; 3F09CCA92428FF8300D00A8C /* ReaderTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderTabView.swift; sourceTree = ""; }; 3F09CCAD24292EFD00D00A8C /* ReaderTabItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderTabItem.swift; sourceTree = ""; }; @@ -11801,15 +11803,16 @@ 3FA6405A2670CCD40064401E /* Info.plist */, 3F762E9226784A950088CD45 /* Logger.swift */, 3FE39A4326F8391D006E2B3A /* Screens */, - 3FA640592670CCD40064401E /* UITestsFoundation.h */, EA85B7A92A6860370096E097 /* TestObserver.swift */, + 3FA640592670CCD40064401E /* UITestsFoundation.h */, 3F762E9426784B540088CD45 /* WireMock.swift */, 3F107B1829B6F7E0009B3658 /* XCTestCase+Utils.swift */, 3F6A8CDF2A246357009DBC2B /* XCUIApplication+SavePassword.swift */, + D8E7529A2A29DC4C00E73B2D /* XCUIApplication+ScrollDownToElement.swift */, 3FB5C2B227059AC8007D0ECE /* XCUIElement+Scroll.swift */, + 3F03F2BC2B45041E00A9CE99 /* XCUIElement+TapUntil.swift */, 3F762E9A26784D2A0088CD45 /* XCUIElement+Utils.swift */, 3F762E9826784CC90088CD45 /* XCUIElementQuery+Utils.swift */, - D8E7529A2A29DC4C00E73B2D /* XCUIApplication+ScrollDownToElement.swift */, ); path = UITestsFoundation; sourceTree = ""; @@ -22942,6 +22945,7 @@ 3F2F855A26FAF227000FCDA5 /* EditorNoticeComponent.swift in Sources */, 01281E9A2A0456CB00464F8F /* DomainsSelectionScreen.swift in Sources */, 3F2F855D26FAF227000FCDA5 /* LoginCheckMagicLinkScreen.swift in Sources */, + 3F03F2BD2B45041E00A9CE99 /* XCUIElement+TapUntil.swift in Sources */, EA78189427596B2F00554DFA /* ContactUsScreen.swift in Sources */, D82E087829EEB7AF0098F500 /* DomainsScreen.swift in Sources */, 3F6A8CE02A246357009DBC2B /* XCUIApplication+SavePassword.swift in Sources */,