diff --git a/WordPressAuthenticator.podspec b/WordPressAuthenticator.podspec index c24e204c3..156d5a0a8 100644 --- a/WordPressAuthenticator.podspec +++ b/WordPressAuthenticator.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "WordPressAuthenticator" - s.version = "1.10.6" + s.version = "1.10.7-beta.1" s.summary = "WordPressAuthenticator implements an easy and elegant way to authenticate your WordPress Apps." s.description = <<-DESC @@ -21,6 +21,7 @@ Pod::Spec.new do |s| s.resource_bundles = { 'WordPressAuthenticatorResources': [ 'WordPressAuthenticator/Resources/Assets.xcassets', + 'WordPressAuthenticator/Resources/SupportedEmailClients/*.plist', 'WordPressAuthenticator/Resources/Animations/*.json', 'WordPressAuthenticator/**/*.{storyboard,xib}' ] diff --git a/WordPressAuthenticator.xcodeproj/project.pbxproj b/WordPressAuthenticator.xcodeproj/project.pbxproj index 974446116..1dde45a37 100644 --- a/WordPressAuthenticator.xcodeproj/project.pbxproj +++ b/WordPressAuthenticator.xcodeproj/project.pbxproj @@ -10,6 +10,11 @@ 020BE74A23B0BD2E007FE54C /* WordPressAuthenticatorDisplayImages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 020BE74923B0BD2E007FE54C /* WordPressAuthenticatorDisplayImages.swift */; }; 1A21EE9822832BC300C940C6 /* WordPressComOAuthClientFacade+Swift.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A21EE9722832BC200C940C6 /* WordPressComOAuthClientFacade+Swift.swift */; }; 1A4095182271AEFC009AA86D /* WPAuthenticator-Swift.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A4095152271AEFC009AA86D /* WPAuthenticator-Swift.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 3F550D4E23DA429B007E5897 /* AppSelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F550D4D23DA429B007E5897 /* AppSelectorTests.swift */; }; + 3F550D5123DA4A9C007E5897 /* LinkMailPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F550D5023DA4A9C007E5897 /* LinkMailPresenter.swift */; }; + 3F550D5323DA4AC6007E5897 /* URLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F550D5223DA4AC6007E5897 /* URLHandler.swift */; }; + 3FFF2FC123D7ED7C00D38C77 /* EmailClients.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3FFF2FC023D7ED7C00D38C77 /* EmailClients.plist */; }; + 3FFF2FC323D7F53200D38C77 /* AppSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FFF2FC223D7F53200D38C77 /* AppSelector.swift */; }; 7A7A9B9CD2D81959F9AB9AF6 /* Pods_WordPressAuthenticator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C736FF243DE333FCAB1C2614 /* Pods_WordPressAuthenticator.framework */; }; 982C8E7923021C20003F1BA0 /* LoginPrologueLoginMethodViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 982C8E7823021C20003F1BA0 /* LoginPrologueLoginMethodViewController.swift */; }; 98AA5A5720AA1A7000A5958A /* WPHelpIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98AA5A5620AA1A7000A5958A /* WPHelpIndicatorView.swift */; }; @@ -149,6 +154,11 @@ 276354F054C34AD36CA32AB6 /* Pods-WordPressAuthenticator.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressAuthenticator.release-alpha.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressAuthenticator/Pods-WordPressAuthenticator.release-alpha.xcconfig"; sourceTree = ""; }; 33FEF45B466FF8EAAE5F3923 /* Pods-WordPressAuthenticator.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressAuthenticator.release.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressAuthenticator/Pods-WordPressAuthenticator.release.xcconfig"; sourceTree = ""; }; 37AFD4EF492B00CA7AEC11A3 /* Pods-WordPressAuthenticatorTests.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressAuthenticatorTests.release-alpha.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressAuthenticatorTests/Pods-WordPressAuthenticatorTests.release-alpha.xcconfig"; sourceTree = ""; }; + 3F550D4D23DA429B007E5897 /* AppSelectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSelectorTests.swift; sourceTree = ""; }; + 3F550D5023DA4A9C007E5897 /* LinkMailPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkMailPresenter.swift; sourceTree = ""; }; + 3F550D5223DA4AC6007E5897 /* URLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLHandler.swift; sourceTree = ""; }; + 3FFF2FC023D7ED7C00D38C77 /* EmailClients.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = EmailClients.plist; sourceTree = ""; }; + 3FFF2FC223D7F53200D38C77 /* AppSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSelector.swift; sourceTree = ""; }; 5A441EC80D2B8D2209C2E228 /* Pods_WordPressAuthenticatorTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WordPressAuthenticatorTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8F7217C3F7A6285D9C6CF786 /* Pods-WordPressAuthenticator.release-internal.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressAuthenticator.release-internal.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressAuthenticator/Pods-WordPressAuthenticator.release-internal.xcconfig"; sourceTree = ""; }; 982C8E7823021C20003F1BA0 /* LoginPrologueLoginMethodViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginPrologueLoginMethodViewController.swift; sourceTree = ""; }; @@ -294,6 +304,39 @@ path = Private; sourceTree = ""; }; + 3F550D4B23DA3B59007E5897 /* SupportedEmailClients */ = { + isa = PBXGroup; + children = ( + 3FFF2FC023D7ED7C00D38C77 /* EmailClients.plist */, + ); + path = SupportedEmailClients; + sourceTree = ""; + }; + 3F550D4C23DA4191007E5897 /* UI */ = { + isa = PBXGroup; + children = ( + ); + path = UI; + sourceTree = ""; + }; + 3F550D4F23DA4A6B007E5897 /* Email Client Picker */ = { + isa = PBXGroup; + children = ( + 3FFF2FC223D7F53200D38C77 /* AppSelector.swift */, + 3F550D5023DA4A9C007E5897 /* LinkMailPresenter.swift */, + 3F550D5223DA4AC6007E5897 /* URLHandler.swift */, + ); + path = "Email Client Picker"; + sourceTree = ""; + }; + 3F550D5423DA5094007E5897 /* Email Client Picker */ = { + isa = PBXGroup; + children = ( + 3F550D4D23DA429B007E5897 /* AppSelectorTests.swift */, + ); + path = "Email Client Picker"; + sourceTree = ""; + }; 6205895375D954F46B1DFE53 /* Pods */ = { isa = PBXGroup; children = ( @@ -477,6 +520,7 @@ children = ( B5A5274020B478160065BE81 /* Animations */, B5E07FF3208FD13800657A9A /* Assets.xcassets */, + 3F550D4B23DA3B59007E5897 /* SupportedEmailClients */, ); path = Resources; sourceTree = ""; @@ -519,6 +563,7 @@ children = ( CE1B18CA20EEC31000BECC3F /* Credentials */, B5609099208A4EAF00399AE4 /* Authenticator */, + 3F550D4F23DA4A6B007E5897 /* Email Client Picker */, B560909B208A4EB000399AE4 /* Extensions */, B5ED7917207E993E00A8FD8C /* Logging */, B5609098208A4EAF00399AE4 /* Model */, @@ -538,9 +583,11 @@ B5ED7901207E976500A8FD8C /* WordPressAuthenticatorTests */ = { isa = PBXGroup; children = ( + 3F550D5423DA5094007E5897 /* Email Client Picker */, B501C03D208FC52500D1E58F /* Authenticator */, B501C03B208FC52400D1E58F /* Model */, B501C03F208FC52500D1E58F /* Services */, + 3F550D4C23DA4191007E5897 /* UI */, B5ED7904207E976500A8FD8C /* Info.plist */, ); path = WordPressAuthenticatorTests; @@ -699,6 +746,7 @@ B5609118208A555600399AE4 /* SearchTableViewCell.xib in Resources */, B560913F208A563800399AE4 /* Login.storyboard in Resources */, B5609137208A563800399AE4 /* EmailMagicLink.storyboard in Resources */, + 3FFF2FC123D7ED7C00D38C77 /* EmailClients.plist in Resources */, FF629D9622393500004C4106 /* WordPressAuthenticator.podspec in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -880,6 +928,7 @@ CE30A2AD2257CECC00DF3CDA /* AuthenticatorCredentials.swift in Sources */, B5609145208A563800399AE4 /* LoginViewController.swift in Sources */, B5609139208A563800399AE4 /* LoginEmailViewController.swift in Sources */, + 3F550D5323DA4AC6007E5897 /* URLHandler.swift in Sources */, 98C9195B2308E3DA00A90E12 /* AppleAuthenticator.swift in Sources */, B56090F9208A533200399AE4 /* WordPressAuthenticator+Events.swift in Sources */, 020BE74A23B0BD2E007FE54C /* WordPressAuthenticatorDisplayImages.swift in Sources */, @@ -915,11 +964,13 @@ B560911F208A555E00399AE4 /* SignupGoogleViewController.swift in Sources */, B5609142208A563800399AE4 /* LoginNavigationController.swift in Sources */, B56090E4208A4F9D00399AE4 /* WPNUXMainButton.m in Sources */, + 3FFF2FC323D7F53200D38C77 /* AppSelector.swift in Sources */, B560913B208A563800399AE4 /* LoginSelfHostedViewController.swift in Sources */, B5609136208A563800399AE4 /* Login2FAViewController.swift in Sources */, B56090E1208A4F9D00399AE4 /* WPWalkthroughTextField.m in Sources */, B56090EF208A527000399AE4 /* WPStyleGuide+Login.swift in Sources */, B56090D0208A4F5400399AE4 /* NUXViewControllerBase.swift in Sources */, + 3F550D5123DA4A9C007E5897 /* LinkMailPresenter.swift in Sources */, B56090DE208A4F9D00399AE4 /* WPWalkthroughOverlayView.m in Sources */, B560910A208A54F800399AE4 /* WordPressComAccountService.swift in Sources */, B56090FA208A533200399AE4 /* WordPressAuthenticator.swift in Sources */, @@ -931,6 +982,7 @@ buildActionMask = 2147483647; files = ( B501C045208FC68700D1E58F /* LoginFieldsValidationTests.swift in Sources */, + 3F550D4E23DA429B007E5897 /* AppSelectorTests.swift in Sources */, CE16177821B70C1A00B82A47 /* WordPressAuthenticatorDisplayTextTests.swift in Sources */, B501C048208FC79C00D1E58F /* LoginFacadeTests.m in Sources */, B501C046208FC6A700D1E58F /* WordPressAuthenticatorTests.swift in Sources */, diff --git a/WordPressAuthenticator/Email Client Picker/AppSelector.swift b/WordPressAuthenticator/Email Client Picker/AppSelector.swift new file mode 100644 index 000000000..b6ed96773 --- /dev/null +++ b/WordPressAuthenticator/Email Client Picker/AppSelector.swift @@ -0,0 +1,136 @@ +import MessageUI +import UIKit + +/// App selector that selects an app from a list and opens it +/// Note: it's a wrapper of UIAlertController (which cannot be sublcassed) +class AppSelector { + // the action sheet that will contain the list of apps that can be called + let alertController: UIAlertController + + /// initializes the picker with a dictionary. Initialization will fail if an empty/invalid app list is passed + /// - Parameters: + /// - appList: collection of apps to be added to the selector + /// - defaultAction: default action, if not nil, will be the first element of the list + /// - sourceView: the sourceView to anchor the action sheet to + /// - urlHandler: object that handles app URL schemes; defaults to UIApplication.shared + init?(with appList: [String: String], + defaultAction: UIAlertAction? = nil, + sourceView: UIView, + urlHandler: URLHandler = UIApplication.shared) { + /// inline method that builds a list of app calls to be inserted in the action sheet + func makeAlertActions(from appList: [String: String]) -> [UIAlertAction]? { + guard !appList.isEmpty else { + return nil + } + + var actions = [UIAlertAction]() + for (name, urlString) in appList { + guard let url = URL(string: urlString), urlHandler.canOpenURL(url) else { + continue + } + actions.append(UIAlertAction(title: AppSelectorTitles(rawValue: name)?.localized ?? name, style: .default) { action in + urlHandler.open(url, options: [:], completionHandler: nil) + }) + } + + guard !actions.isEmpty else { + return nil + } + //sort the apps alphabetically + actions = actions.sorted { $0.title ?? "" < $1.title ?? "" } + actions.append(UIAlertAction(title: AppSelectorTitles.cancel.localized, style: .cancel, handler: nil)) + + if let action = defaultAction { + actions.insert(action, at: 0) + } + return actions + } + + guard let appCalls = makeAlertActions(from: appList) else { + return nil + } + + alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + alertController.popoverPresentationController?.sourceView = sourceView + alertController.popoverPresentationController?.sourceRect = sourceView.bounds + appCalls.forEach { + alertController.addAction($0) + } + } +} + + +/// Initializers for Email Picker +extension AppSelector { + /// initializes the picker with a plist file in a specified bundle + convenience init?(with plistFile: String, + in bundle: Bundle, + defaultAction: UIAlertAction? = nil, + sourceView: UIView) { + + guard let plistPath = bundle.path(forResource: plistFile, ofType: "plist"), + let availableApps = NSDictionary(contentsOfFile: plistPath) as? [String: String] else { + return nil + } + self.init(with: availableApps, + defaultAction: defaultAction, + sourceView: sourceView) + } + + /// Convenience init for a picker that calls supported email clients apps, defined in EmailClients.plist + convenience init?(sourceView: UIView) { + guard let bundlePath = Bundle(for: type(of: self)) + .path(forResource: "WordPressAuthenticatorResources", ofType: "bundle"), + let wpAuthenticatorBundle = Bundle(path: bundlePath) else { + return nil + } + + let plistFile = "EmailClients" + var defaultAction: UIAlertAction? + + // if available, prepend apple mail + if MFMailComposeViewController.canSendMail(), let url = URL(string: "message://") { + defaultAction = UIAlertAction(title: AppSelectorTitles.appleMail.localized, style: .default) { action in + UIApplication.shared.open(url) + } + } + self.init(with: plistFile, + in: wpAuthenticatorBundle, + defaultAction: defaultAction, + sourceView: sourceView) + } +} + + +/// Localizable app selector titles +enum AppSelectorTitles: String { + case appleMail + case gmail + case airmail + case msOutlook + case spark + case yahooMail + case fastmail + case cancel + + var localized: String { + switch self { + case .appleMail: + return NSLocalizedString("Mail (Default)", comment: "Option to select the Apple Mail app when logging in with magic links") + case .gmail: + return NSLocalizedString("Gmail", comment: "Option to select the Gmail app when logging in with magic links") + case .airmail: + return NSLocalizedString("Airmail", comment: "Option to select the Airmail app when logging in with magic links") + case .msOutlook: + return NSLocalizedString("Microsoft Outlook", comment: "Option to select the Microsft Outlook app when logging in with magic links") + case .spark: + return NSLocalizedString("Spark", comment: "Option to select the Spark email app when logging in with magic links") + case .yahooMail: + return NSLocalizedString("Yahoo Mail", comment: "Option to select the Yahoo Mail app when logging in with magic links") + case .fastmail: + return NSLocalizedString("Fastmail", comment: "Option to select the Fastmail app when logging in with magic links") + case .cancel: + return NSLocalizedString("Cancel", comment: "Option to cancel the email app selection when logging in with magic links") + } + } +} diff --git a/WordPressAuthenticator/Email Client Picker/LinkMailPresenter.swift b/WordPressAuthenticator/Email Client Picker/LinkMailPresenter.swift new file mode 100644 index 000000000..f758fb2b3 --- /dev/null +++ b/WordPressAuthenticator/Email Client Picker/LinkMailPresenter.swift @@ -0,0 +1,49 @@ +import MessageUI + + +/// Email picker presenter +class LinkMailPresenter { + + private let emailAddress: String + + init(emailAddress: String) { + self.emailAddress = emailAddress + } + + /// Presents the available mail clients in an action sheet. If none is available, + /// Falls back to Apple Mail and opens it. + /// If not even Apple Mail is available, presents an alert to check your email + /// - Parameters: + /// - viewController: the UIViewController that will present the action sheet + /// - appSelector: the app picker that contains the available clients. Nil if no clients are available + /// reads the supported email clients from EmailClients.plist + func presentEmailClients(on viewController: UIViewController, + appSelector: AppSelector?) { + + guard let picker = appSelector else { + // fall back to Apple Mail if no other clients are installed + if MFMailComposeViewController.canSendMail(), let url = URL(string: "message://") { + UIApplication.shared.open(url) + } else { + showAlertToCheckEmail(on: viewController) + } + return + } + viewController.present(picker.alertController, animated: true) + } + + private func showAlertToCheckEmail(on viewController: UIViewController) { + let title = NSLocalizedString("Check your email!", + comment: "Alert title for check your email during logIn/signUp.") + + let message = String.localizedStringWithFormat(NSLocalizedString("We just emailed a link to %@. Please check your mail app and tap the link to log in.", + comment: "message to ask a user to check their email for a WordPress.com email"), emailAddress) + + let alertController = UIAlertController(title: title, + message: message, + preferredStyle: .alert) + alertController.addCancelActionWithTitle(NSLocalizedString("OK", + comment: "Button title. An acknowledgement of the message displayed in a prompt.")) + viewController.present(alertController, animated: true, completion: nil) + } +} diff --git a/WordPressAuthenticator/Email Client Picker/URLHandler.swift b/WordPressAuthenticator/Email Client Picker/URLHandler.swift new file mode 100644 index 000000000..24a92f2a3 --- /dev/null +++ b/WordPressAuthenticator/Email Client Picker/URLHandler.swift @@ -0,0 +1,13 @@ +/// Generic type that handles URL Schemes +protocol URLHandler { + /// checks if the specified URL can be opened + func canOpenURL(_ url: URL) -> Bool + /// opens the specified URL + func open(_ url: URL, + options: [UIApplication.OpenExternalURLOptionsKey : Any], + completionHandler completion: ((Bool) -> Void)?) +} + +/// conforms UIApplication to URLHandler to allow dependency injection +extension UIApplication: URLHandler {} + diff --git a/WordPressAuthenticator/NUX/NUXLinkMailViewController.swift b/WordPressAuthenticator/NUX/NUXLinkMailViewController.swift index 395166101..4bb212137 100644 --- a/WordPressAuthenticator/NUX/NUXLinkMailViewController.swift +++ b/WordPressAuthenticator/NUX/NUXLinkMailViewController.swift @@ -1,5 +1,4 @@ import UIKit -import MessageUI import WordPressShared @@ -105,30 +104,12 @@ class NUXLinkMailViewController: LoginViewController { } } } - if MFMailComposeViewController.canSendMail() { - let url = URL(string: "message://")! - UIApplication.shared.open(url) - } else if let googleMailURL = URL(string: "googlegmail://"), - UIApplication.shared.canOpenURL(googleMailURL) { - UIApplication.shared.open(googleMailURL) - } else { - showAlertToCheckEmail() - } - } - func showAlertToCheckEmail() { - let title = NSLocalizedString("Please check your email", comment: "Alert title for check your email during logIn/signUp.") - let message = NSLocalizedString("Please open your email app and look for an email from WordPress.com.", comment: "Message to ask the user to check their email and look for a WordPress.com email.") - - let alertController = UIAlertController(title: title, - message: message, - preferredStyle: .alert) - alertController.addCancelActionWithTitle(NSLocalizedString("OK", - comment: "Button title. An acknowledgement of the message displayed in a prompt.")) - self.present(alertController, animated: true, completion: nil) + let linkMailPresenter = LinkMailPresenter(emailAddress: loginFields.username) + let appSelector = AppSelector(sourceView: sender) + linkMailPresenter.presentEmailClients(on: self, appSelector: appSelector) } - @IBAction func handleUsePasswordTapped(_ sender: UIButton) { WordPressAuthenticator.track(.loginMagicLinkExited) } diff --git a/WordPressAuthenticator/Resources/SupportedEmailClients/EmailClients.plist b/WordPressAuthenticator/Resources/SupportedEmailClients/EmailClients.plist new file mode 100644 index 000000000..e085bae01 --- /dev/null +++ b/WordPressAuthenticator/Resources/SupportedEmailClients/EmailClients.plist @@ -0,0 +1,18 @@ + + + + + gmail + googlegmail:// + airmail + airmail:// + msOutlook + ms-outlook:// + spark + readdle-spark:// + yahooMail + ymail:// + fastmail + fastmail:// + + diff --git a/WordPressAuthenticatorTests/Email Client Picker/AppSelectorTests.swift b/WordPressAuthenticatorTests/Email Client Picker/AppSelectorTests.swift new file mode 100644 index 000000000..5bc445af5 --- /dev/null +++ b/WordPressAuthenticatorTests/Email Client Picker/AppSelectorTests.swift @@ -0,0 +1,71 @@ +import XCTest +@testable import WordPressAuthenticator + + +struct URLMocks { + + static let mockAppList = ["gmail": "googlemail://", "airmail": "airmail://"] +} + +class MockUrlHandler: URLHandler { + + var shouldOpenUrls = true + + var canOpenUrlExpectation: XCTestExpectation? + var openUrlExpectation: XCTestExpectation? + + func canOpenURL(_ url: URL) -> Bool { + canOpenUrlExpectation?.fulfill() + canOpenUrlExpectation = nil + return shouldOpenUrls + } + + func open(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey : Any], completionHandler completion: ((Bool) -> Void)?) { + openUrlExpectation?.fulfill() + } +} + +class AppSelectorTests: XCTestCase { + + func testSelectorInitializationSuccess() { + // Given + let urlHandler = MockUrlHandler() + urlHandler.canOpenUrlExpectation = expectation(description: "canOpenUrl called") + // When + let appSelector = AppSelector(with: URLMocks.mockAppList, sourceView: UIView(), urlHandler: urlHandler) + // Then + XCTAssertNotNil(appSelector) + XCTAssertNotNil(appSelector?.alertController) + XCTAssertEqual(appSelector!.alertController.actions.count, 3) + waitForExpectations(timeout: 4) { error in + if let error = error { + XCTFail("waitForExpectationsWithTimeout errored: \(error)") + } + } + } + + func testSelectorInitializationFailsWithNoApps() { + // Given + let urlHandler = MockUrlHandler() + // When + let appSelector = AppSelector(with: [:], sourceView: UIView(), urlHandler: urlHandler) + // Then + XCTAssertNil(appSelector) + } + + func testSelectorInitializationFailsWithInvalidUrl() { + // Given + let urlHandler = MockUrlHandler() + urlHandler.canOpenUrlExpectation = expectation(description: "canOpenUrl called") + urlHandler.shouldOpenUrls = false + // When + let appSelector = AppSelector(with: URLMocks.mockAppList, sourceView: UIView(), urlHandler: urlHandler) + // Then + XCTAssertNil(appSelector) + waitForExpectations(timeout: 4) { error in + if let error = error { + XCTFail("waitForExpectationsWithTimeout errored: \(error)") + } + } + } +}