/* * Copyright (c) Meta Platforms, Inc. and affiliates. * All rights reserved. * * This source code is licensed under the license found in the * LICENSE file in the root directory of this source tree. */ @testable import FBSDKShareKit import FBSDKCoreKit import Photos import TestTools import UIKit import XCTest final class ShareDialogTests: XCTestCase { // swiftlint:disable implicitly_unwrapped_optional var dialog: ShareDialog! var delegate: TestSharingDelegate! var internalURLOpener: TestInternalURLOpener! var internalUtility: TestInternalUtility! var settings: TestSettings! var bridgeAPIRequestFactory: TestBridgeAPIRequestFactory! var bridgeAPIRequestOpener: TestBridgeAPIRequestOpener! var socialComposeViewController: TestSocialComposeViewController! var socialComposeViewControllerFactory: TestSocialComposeViewControllerFactory! var windowFinder: TestWindowFinder! var errorFactory: TestErrorFactory! var eventLogger: TestShareEventLogger! var mediaLibrarySearcher: TestMediaLibrarySearcher! // swiftlint:enable implicitly_unwrapped_optional override func setUp() { super.setUp() AccessToken.current = nil TestShareUtility.reset() delegate = TestSharingDelegate() internalURLOpener = TestInternalURLOpener() internalUtility = TestInternalUtility() settings = TestSettings() bridgeAPIRequestFactory = TestBridgeAPIRequestFactory() bridgeAPIRequestOpener = TestBridgeAPIRequestOpener() socialComposeViewController = TestSocialComposeViewController() socialComposeViewControllerFactory = TestSocialComposeViewControllerFactory() socialComposeViewControllerFactory.stubbedSocialComposeViewController = socialComposeViewController windowFinder = TestWindowFinder() errorFactory = TestErrorFactory() eventLogger = TestShareEventLogger() mediaLibrarySearcher = TestMediaLibrarySearcher() ShareDialog.setDependencies( .init( internalURLOpener: internalURLOpener, internalUtility: internalUtility, settings: settings, shareUtility: TestShareUtility.self, bridgeAPIRequestFactory: bridgeAPIRequestFactory, bridgeAPIRequestOpener: bridgeAPIRequestOpener, socialComposeViewControllerFactory: socialComposeViewControllerFactory, windowFinder: windowFinder, errorFactory: errorFactory, eventLogger: eventLogger, mediaLibrarySearcher: mediaLibrarySearcher ) ) ShareCameraEffectContent.setDependencies( .init( internalUtility: internalUtility, errorFactory: errorFactory ) ) } override func tearDown() { dialog = nil internalURLOpener = nil internalUtility = nil settings = nil bridgeAPIRequestFactory = nil bridgeAPIRequestOpener = nil socialComposeViewController = nil socialComposeViewControllerFactory = nil windowFinder = nil errorFactory = nil eventLogger = nil mediaLibrarySearcher = nil ShareDialog.resetDependencies() TestShareUtility.reset() ShareCameraEffectContent.resetDependencies() AccessToken.current = nil super.tearDown() } // MARK: - Type Dependencies func testDefaultDependencies() throws { ShareDialog.resetDependencies() let dependencies = try ShareDialog.getDependencies() XCTAssertIdentical( dependencies.internalURLOpener, ShareUIApplication.shared, .DefaultDependencies.usesInternalURLOpenerByDefault ) XCTAssertIdentical( dependencies.internalUtility, InternalUtility.shared, .DefaultDependencies.usesInternalUtilityByDefault ) XCTAssertIdentical(dependencies.settings, Settings.shared, .DefaultDependencies.usesSettingsByDefault) XCTAssertTrue(dependencies.shareUtility is _ShareUtility.Type, .DefaultDependencies.usesShareUtilityByDefault) XCTAssertTrue( dependencies.bridgeAPIRequestFactory is ShareBridgeAPIRequestFactory, .DefaultDependencies.usesShareBridgeAPIRequestFactoryByDefault ) XCTAssertIdentical( dependencies.bridgeAPIRequestOpener, _BridgeAPI.shared, .DefaultDependencies.usesBridgeAPIByDefault ) XCTAssertTrue( dependencies.socialComposeViewControllerFactory is SocialComposeViewControllerFactory, .DefaultDependencies.usesSocialComposeViewControllerFactoryByDefault ) XCTAssertIdentical( dependencies.windowFinder, InternalUtility.shared, .DefaultDependencies.usesInternalUtilityAsWindowFinderByDefault ) XCTAssertTrue(dependencies.errorFactory is _ErrorFactory, .DefaultDependencies.usesErrorFactoryByDefault) XCTAssertIdentical( dependencies.eventLogger as AnyObject, AppEvents.shared, .DefaultDependencies.usesAppEventsByDefault ) XCTAssertIdentical( dependencies.mediaLibrarySearcher as AnyObject, PHImageManager.default(), .DefaultDependencies.usesPHImageManagerAsMediaLibrarySearcherByDefault ) } func testCustomDependencies() throws { let dependencies = try ShareDialog.getDependencies() XCTAssertIdentical( dependencies.internalURLOpener, internalURLOpener, .CustomDependencies.usesCustomInternalURLOpener ) XCTAssertIdentical(dependencies.internalUtility, internalUtility, .CustomDependencies.usesCustomInternalUtility) XCTAssertIdentical(dependencies.settings, settings, .CustomDependencies.usesCustomSettings) XCTAssertTrue(dependencies.shareUtility is TestShareUtility.Type, .CustomDependencies.usesCustomShareUtility) XCTAssertIdentical( dependencies.bridgeAPIRequestFactory, bridgeAPIRequestFactory, .CustomDependencies.usesCustomShareBridgeAPIRequestFactory ) XCTAssertIdentical( dependencies.bridgeAPIRequestOpener, bridgeAPIRequestOpener, .CustomDependencies.usesCustomBridgeAPIRequestOpener ) XCTAssertIdentical( dependencies.socialComposeViewControllerFactory as AnyObject, socialComposeViewControllerFactory, .CustomDependencies.usesCustomSocialComposeViewControllerFactory ) XCTAssertIdentical(dependencies.windowFinder, windowFinder, .CustomDependencies.usesCustomWindowFinder) XCTAssertIdentical(dependencies.errorFactory, errorFactory, .CustomDependencies.usesCustomErrorFactory) XCTAssertIdentical(dependencies.eventLogger as AnyObject, eventLogger, .CustomDependencies.usesCustomEventLogger) XCTAssertIdentical( dependencies.mediaLibrarySearcher as AnyObject, mediaLibrarySearcher, .CustomDependencies.usesCustomMediaLibrarySearcher ) } // MARK: - Construction func testInitializer() { let controller = UIViewController() let content = ShareModelTestUtility.linkContent let delegate = TestSharingDelegate() dialog = ShareDialog(viewController: controller, content: content, delegate: delegate) XCTAssertIdentical(dialog.fromViewController, controller, .Construction.createViaClassFactoryMethod) XCTAssertIdentical(dialog.shareContent, content, .Construction.createViaClassFactoryMethod) XCTAssertIdentical(dialog.delegate, delegate, .Construction.createViaClassFactoryMethod) } func testClassFactoryMethod() { let controller = UIViewController() let content = ShareModelTestUtility.linkContent let delegate = TestSharingDelegate() dialog = ShareDialog.dialog(viewController: controller, content: content, delegate: delegate) XCTAssertIdentical(dialog.fromViewController, controller, .Construction.createViaClassFactoryMethod) XCTAssertIdentical(dialog.shareContent, content, .Construction.createViaClassFactoryMethod) XCTAssertIdentical(dialog.delegate, delegate, .Construction.createViaClassFactoryMethod) } func testClassShowMethod() { let controller = UIViewController() let content = ShareModelTestUtility.linkContent let delegate = TestSharingDelegate() let components = WebShareBridgeComponents(methodName: "test", parameters: ["key": "value"]) TestShareUtility.stubbedWebShareBridgeComponents = components dialog = ShareDialog.show(viewController: controller, content: content, delegate: delegate) XCTAssertIdentical(dialog.fromViewController, controller, .Construction.createViaClassShowMethod) XCTAssertIdentical(dialog.shareContent, content, .Construction.createViaClassShowMethod) XCTAssertIdentical(dialog.delegate, delegate, .Construction.createViaClassShowMethod) } // MARK: - Native mode func testCanShowNativeDialogWithoutShareContent() { dialog = createEmptyDialog(mode: .native) internalURLOpener.canOpenURL = true internalUtility.isFacebookAppInstalled = true XCTAssertTrue( dialog.canShow, "A dialog without share content should be showable on a native dialog" ) } func testCanShowNativeLinkContent() { dialog = createEmptyDialog(mode: .native) dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertTrue( dialog.canShow, "A dialog with valid link content should be showable on a native dialog" ) } func testCanShowNativePhotoContent() { dialog = createEmptyDialog(mode: .native) dialog.shareContent = ShareModelTestUtility.photoContent TestShareUtility.validateShareContentShouldThrow = true XCTAssertFalse( dialog.canShow, "Photo content with photos that have web urls should not be showable on a native dialog" ) } func testCanShowNativePhotoContentWithFileURL() { dialog = createEmptyDialog(mode: .native) dialog.shareContent = ShareModelTestUtility.photoContentWithFileURLs XCTAssertTrue( dialog.canShow, "Photo content with photos that have file urls should be showable on a native dialog" ) } func testCanShowNativeVideoContentWithoutPreviewPhoto() { dialog = createEmptyDialog(mode: .native) internalURLOpener.canOpenURL = true dialog.shareContent = ShareModelTestUtility.videoContentWithoutPreviewPhoto XCTAssertTrue( dialog.canShow, "Video content without a preview photo should be showable on a native dialog" ) } func testCanShowNative() { dialog = createEmptyDialog(mode: .native) XCTAssertFalse( dialog.canShow, "A native dialog should not be showable if the application is unable to open a url, this can also occur if the api scheme is not whitelisted in the third party app or if the application cannot handle the share API scheme" // swiftlint:disable:this line_length ) } func testShowNativeDoesValidate() { dialog = createEmptyDialog(mode: .native) dialog.shareContent = ShareModelTestUtility.photoContent internalURLOpener.canOpenURL = true XCTAssertFalse(dialog.show()) } // MARK: - Browser mode func testCanShowBrowser() { dialog = createEmptyDialog(mode: .browser) XCTAssertTrue( dialog.canShow, "A dialog without share content should be showable in a browser" ) dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertTrue( dialog.canShow, "A dialog with link content should be showable in a browser" ) AccessToken.current = SampleAccessTokens.validToken dialog.shareContent = ShareModelTestUtility.photoContentWithFileURLs settings.isAdvertiserTrackingEnabled = true XCTAssertTrue( dialog.canShow, "A dialog with photo content with file urls should be showable in a browser when there is a current access token" ) dialog.shareContent = ShareModelTestUtility.videoContentWithoutPreviewPhoto XCTAssertTrue( dialog.canShow, "A dialog with video content without a preview photo should be showable in a browser when there is a current access token" // swiftlint:disable:this line_length ) } func testValidateBrowser() throws { dialog = createEmptyDialog(mode: .browser) dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertNoThrow(try dialog.validate()) settings.isAdvertiserTrackingEnabled = true dialog.shareContent = ShareModelTestUtility.photoContentWithImages AccessToken.current = SampleAccessTokens.validToken XCTAssertNoThrow(try dialog.validate()) AccessToken.current = nil TestShareUtility.stubbedTestShareContainsPhotos = true AccessToken.current = nil XCTAssertThrowsError(try dialog.validate()) TestShareUtility.reset() TestShareUtility.stubbedTestShareContainsVideos = true dialog.shareContent = ShareModelTestUtility.videoContentWithoutPreviewPhoto XCTAssertThrowsError(try dialog.validate()) } func testSharingViaBrowserWithoutContent() { bridgeAPIRequestFactory.stubbedBridgeAPIRequest = TestBridgeAPIRequest() dialog = createEmptyDialog(mode: .browser) XCTAssertFalse(dialog.show(), .Showing.showingRequiresValidContent) XCTAssertTrue(delegate.sharerDidFailCalled, .Showing.showingRequiresValidContent) } func testSharingViaBrowserWithInvalidLinkContent() { bridgeAPIRequestFactory.stubbedBridgeAPIRequest = TestBridgeAPIRequest() dialog = createEmptyDialog(mode: .browser) let content = ShareModelTestUtility.linkContent content.contentURL = nil dialog.shareContent = content XCTAssertFalse(dialog.show(), .Showing.showingRequiresValidContent) XCTAssertTrue(delegate.sharerDidFailCalled, .Showing.showingRequiresValidContent) } func testSharingViaBrowserWithValidLinkContent() { let request = TestBridgeAPIRequest() bridgeAPIRequestFactory.stubbedBridgeAPIRequest = request let components = WebShareBridgeComponents(methodName: "test", parameters: ["key": "value"]) TestShareUtility.stubbedWebShareBridgeComponents = components let content = ShareModelTestUtility.linkContent validate( shareContent: content, expectValid: true, expectShow: true, mode: .browser ) XCTAssertIdentical( TestShareUtility.capturedWebShareBridgeComponentsContent, content, .Showing.webShareBridgeComponents ) XCTAssertEqual(bridgeAPIRequestFactory.capturedProtocolType, .web, .Showing.bridgeAPIRequest) XCTAssertEqual(bridgeAPIRequestFactory.capturedScheme, URLSchemeEnum.https.rawValue, .Showing.bridgeAPIRequest) XCTAssertEqual(bridgeAPIRequestFactory.capturedMethodName, components.methodName, .Showing.bridgeAPIRequest) XCTAssertEqual( bridgeAPIRequestFactory.capturedParameters as? [String: String], components.parameters as? [String: String], .Showing.bridgeAPIRequest ) XCTAssertNil(bridgeAPIRequestFactory.capturedUserInfo, .Showing.bridgeAPIRequest) let response = BridgeAPIResponse(request: request, error: nil) bridgeAPIRequestOpener.capturedCompletionBlock?(response) XCTAssertTrue(delegate.sharerDidCompleteCalled, .WebDialogDelegate.didCompleteCalled) XCTAssertIdentical( internalUtility.unregisterTransientObjectObject as AnyObject, dialog, .WebDialogDelegate.unregistersTransientObject ) } func testSharingViaBrowserWithValidPhotoContent() { settings.isAdvertiserTrackingEnabled = true let request = TestBridgeAPIRequest() bridgeAPIRequestFactory.stubbedBridgeAPIRequest = request let components = WebShareBridgeComponents(methodName: "test", parameters: ["key": "value"]) TestShareUtility.stubbedWebShareBridgeComponents = components let content = ShareModelTestUtility.photoContentWithImages validate( shareContent: content, expectValid: true, expectShow: true, mode: .browser ) XCTAssertIdentical( TestShareUtility.capturedAsyncWebPhotoContentContent, content, .Showing.webPhotoContent ) let parameters = ["key": "value"] TestShareUtility.capturedAsyncWebPhotoContentCompletion?(true, "test", parameters) XCTAssertEqual(bridgeAPIRequestFactory.capturedProtocolType, .web, .Showing.bridgeAPIRequest) XCTAssertEqual(bridgeAPIRequestFactory.capturedScheme, URLSchemeEnum.https.rawValue, .Showing.bridgeAPIRequest) XCTAssertEqual(bridgeAPIRequestFactory.capturedMethodName, "test", .Showing.bridgeAPIRequest) XCTAssertEqual( bridgeAPIRequestFactory.capturedParameters as? [String: String], parameters, .Showing.bridgeAPIRequest ) XCTAssertNil(bridgeAPIRequestFactory.capturedUserInfo, .Showing.bridgeAPIRequest) let response = BridgeAPIResponse(request: request, error: nil) bridgeAPIRequestOpener.capturedCompletionBlock?(response) XCTAssertTrue(delegate.sharerDidCompleteCalled, .WebDialogDelegate.didCompleteCalled) XCTAssertIdentical( internalUtility.unregisterTransientObjectObject as AnyObject, dialog, .WebDialogDelegate.unregistersTransientObject ) } // MARK: - Web mode func testCanShowWebWithTrackingPermission() { settings.isAdvertiserTrackingEnabled = true dialog = createEmptyDialog(mode: .web) XCTAssertTrue( dialog.canShow, "A dialog without share content should be showable on web" ) dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertTrue( dialog.canShow, "A dialog with link content should be showable on web" ) AccessToken.current = SampleAccessTokens.validToken TestShareUtility.stubbedTestShareContainsPhotos = true TestShareUtility.validateShareContentShouldThrow = true dialog.shareContent = ShareModelTestUtility.photoContent XCTAssertFalse( dialog.canShow, "A dialog with photos should not be showable on web" ) TestShareUtility.reset() dialog.shareContent = ShareModelTestUtility.videoContentWithoutPreviewPhoto TestShareUtility.stubbedTestShareContainsMedia = true XCTAssertFalse( dialog.canShow, "A dialog with content that contains local media should not be showable on web" ) } func testCanShowWebWithoutTrackingPermissions() { dialog = createEmptyDialog(mode: .web) XCTAssertFalse( dialog.canShow, "A dialog without share content should be showable on web" ) dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertFalse( dialog.canShow, "A dialog with link content should not be showable on web" ) } func testValidateWebWithTrackingPermission() throws { settings.isAdvertiserTrackingEnabled = true dialog = createEmptyDialog(mode: .web) dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertNoThrow(try dialog.validate()) AccessToken.current = SampleAccessTokens.validToken dialog.shareContent = ShareModelTestUtility.photoContent TestShareUtility.validateShareContentShouldThrow = true XCTAssertThrowsError( try dialog.validate(), "A dialog with photo content that points to remote urls should not be considered valid on web" ) TestShareUtility.reset() dialog.shareContent = ShareModelTestUtility.photoContentWithImages TestShareUtility.stubbedTestShareContainsPhotos = true XCTAssertThrowsError( try dialog.validate(), "A dialog with photo content that is already loaded should not be considered valid on web" ) TestShareUtility.reset() dialog.shareContent = ShareModelTestUtility.photoContentWithFileURLs TestShareUtility.stubbedTestShareContainsPhotos = true XCTAssertThrowsError( try dialog.validate(), "A dialog with photo content that points to file urls should not be considered valid on web" ) TestShareUtility.reset() dialog.shareContent = ShareModelTestUtility.videoContentWithoutPreviewPhoto TestShareUtility.stubbedTestShareContainsMedia = true XCTAssertThrowsError( try dialog.validate(), "A dialog that includes local media should not be considered valid on web" ) TestShareUtility.reset() AccessToken.current = nil TestShareUtility.stubbedTestShareContainsVideos = true XCTAssertThrowsError( try dialog.validate(), "A dialog with content but no access token should not be considered valid on web" ) } // MARK: - Feed browser mode func testCanShowFeedBrowser() { dialog = createEmptyDialog(mode: .feedBrowser) XCTAssertTrue( dialog.canShow, "A dialog without content should be showable in a browser feed" ) dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertTrue( dialog.canShow, "A dialog with link content should be showable in a browser feed" ) dialog.shareContent = ShareModelTestUtility.photoContent XCTAssertFalse( dialog.canShow, "A dialog with photo content should not be showable in a browser feed" ) dialog.shareContent = ShareModelTestUtility.videoContentWithoutPreviewPhoto XCTAssertFalse( dialog.canShow, "A dialog with video content that has no preview photo should not be showable in a browser feed" ) } func testValidateFeedBrowser() throws { dialog = createEmptyDialog(mode: .feedBrowser) dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertNoThrow(try dialog.validate()) dialog.shareContent = ShareModelTestUtility.photoContentWithImages XCTAssertThrowsError(try dialog.validate()) dialog.shareContent = ShareModelTestUtility.videoContentWithoutPreviewPhoto XCTAssertThrowsError(try dialog.validate()) } // MARK: - Share sheet mode func testValidateShareSheet() throws { dialog = createEmptyDialog(mode: .shareSheet) dialog.shareContent = ShareModelTestUtility.linkContentWithoutQuote XCTAssertNoThrow( try dialog.validate(), "Should not throw an error when validating link content without quotes" ) dialog.shareContent = ShareModelTestUtility.photoContentWithImages XCTAssertNoThrow( try dialog.validate(), "Should not throw an error when validating photo content with images" ) dialog.shareContent = ShareModelTestUtility.photoContent XCTAssertThrowsError( try dialog.validate(), "Should throw an error when validating photo content on a share sheet" ) dialog.shareContent = ShareModelTestUtility.videoContentWithoutPreviewPhoto XCTAssertThrowsError( try dialog.validate(), "Should throw an error when validating video content without a preview photo on a share sheet" ) } func testThatInitialTextIsSetCorrectlyWhenShareExtensionIsAvailable() throws { dialog = createEmptyDialog(mode: .shareSheet) let content = ShareModelTestUtility.linkContent content.hashtag = Hashtag("#hashtag") TestShareUtility.stubbedHashtagString = "#hashtag" content.quote = "a quote" dialog.shareContent = content internalUtility.isFacebookAppInstalled = true internalURLOpener.canOpenURL = true settings.appID = "appID" let viewController = UIViewController() dialog.fromViewController = viewController XCTAssertTrue(dialog.show()) try validateInitialText( capturedText: socialComposeViewController.capturedInitialText, expectedAppID: "appID", expectedHashtag: "#hashtag", expectedQuotes: ["a quote"] ) } func testPassingValidationForLinkQuoteWithValidShareExtensionVersion() { internalUtility.isFacebookAppInstalled = true validate( shareContent: ShareModelTestUtility.linkContent, expectValid: true, expectShow: true, mode: .shareSheet ) } func testValidateWithErrorReturnsFalseForMMPIfAValidShareExtensionVersionIsNotAvailable() { TestShareUtility.validateShareContentShouldThrow = true validate( shareContent: ShareModelTestUtility.mediaContent, expectValid: false, expectShow: false, mode: .shareSheet, nonSupportedScheme: "fbapi20160328:/" ) } func testThatValidateWithErrorReturnsTrueForMMPIfAValidShareExtensionVersionIsAvailable() { internalUtility.isFacebookAppInstalled = true validate( shareContent: ShareModelTestUtility.mediaContent, expectValid: true, expectShow: true, mode: .shareSheet ) } func testThatValidateWithErrorReturnsFalseForMMPWithMoreThan1Video() { validate( shareContent: ShareModelTestUtility.multiVideoMediaContent, expectValid: false, expectShow: false, mode: .shareSheet ) } // MARK: - Feed web mode func testCanShowFeedWebWithTrackingPermission() { settings.isAdvertiserTrackingEnabled = true dialog = createEmptyDialog(mode: .feedWeb) XCTAssertTrue( dialog.canShow, "A dialog without content should be showable in a web feed" ) dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertTrue( dialog.canShow, "A dialog with link content should be showable in a web feed" ) dialog.shareContent = ShareModelTestUtility.photoContent XCTAssertFalse( dialog.canShow, "A dialog with photo content should not be showable in a web feed" ) dialog.shareContent = ShareModelTestUtility.videoContentWithoutPreviewPhoto XCTAssertFalse( dialog.canShow, "A dialog with video content and no preview photo should not be showable in a web feed" ) } func testCanShowFeedWebWithoutTrackingPermission() { settings.isAdvertiserTrackingEnabled = false dialog = createEmptyDialog(mode: .feedWeb) XCTAssertFalse( dialog.canShow, "A dialog without tracking permissions should not be showable in a web feed" ) } func testValidateFeedWebWithTrackingPermission() throws { settings.isAdvertiserTrackingEnabled = true dialog = createEmptyDialog(mode: .feedWeb) dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertNoThrow(try dialog.validate()) dialog.shareContent = ShareModelTestUtility.photoContentWithImages XCTAssertThrowsError(try dialog.validate()) dialog.shareContent = ShareModelTestUtility.videoContentWithoutPreviewPhoto XCTAssertThrowsError(try dialog.validate()) } func testValidateFeedWebWithoutTrackingPermission() throws { settings.isAdvertiserTrackingEnabled = false dialog = createEmptyDialog(mode: .feedWeb) dialog.shareContent = ShareModelTestUtility.linkContent XCTAssertThrowsError( try dialog.validate(), "A web view dialog without tracking permission should not pass validation" ) } // MARK: - Automatic mode func testCameraShareModesWhenNativeUnavailable() { dialog = createEmptyDialog(mode: .automatic) dialog.shareContent = ShareModelTestUtility.cameraEffectContent XCTAssertThrowsError(try dialog.validate()) } // MARK: - Multiple modes func testCameraShareModesWhenNativeAvailable() throws { dialog = createEmptyDialog(mode: .automatic) dialog.shareContent = ShareModelTestUtility.cameraEffectContent internalURLOpener.canOpenURL = true internalUtility.isFacebookAppInstalled = true // Check supported modes dialog.mode = .automatic XCTAssertNoThrow(try dialog.validate()) dialog.mode = .native XCTAssertNoThrow(try dialog.validate()) // Check unsupported modes dialog.mode = .web XCTAssertThrowsError(try dialog.validate()) dialog.mode = .browser XCTAssertThrowsError(try dialog.validate()) dialog.mode = .shareSheet XCTAssertThrowsError(try dialog.validate()) dialog.mode = .feedWeb XCTAssertThrowsError(try dialog.validate()) dialog.mode = .feedBrowser XCTAssertThrowsError(try dialog.validate()) } // MARK: - WebDialogDelegate func testWebDialogDelegateCancellation() { dialog = createEmptyDialog(mode: .web) let webDialog = _WebDialog(name: "test") webDialog.delegate = dialog dialog.webDialog = webDialog dialog.webDialogDidCancel(webDialog) XCTAssertNil(dialog.webDialog, .WebDialogDelegate.clearsWebDialog) XCTAssertTrue(delegate.sharerDidCancelCalled, .WebDialogDelegate.didCancelCalled) XCTAssertIdentical(delegate.sharerDidCancelSharer, dialog, .WebDialogDelegate.didCancelCalled) XCTAssertIdentical( internalUtility.unregisterTransientObjectObject as AnyObject, dialog, .WebDialogDelegate.unregistersTransientObject ) } func testWebDialogDelegateFailure() throws { dialog = createEmptyDialog(mode: .web) let webDialog = _WebDialog(name: "test") webDialog.delegate = dialog dialog.webDialog = webDialog let error = TestSDKError(type: .unknown) dialog.webDialog(webDialog, didFailWithError: error) XCTAssertNil(dialog.webDialog, .WebDialogDelegate.clearsWebDialog) XCTAssertTrue(delegate.sharerDidFailCalled, .WebDialogDelegate.didFailCalled) XCTAssertIdentical(delegate.sharerDidFailSharer, dialog, .WebDialogDelegate.didFailCalled) XCTAssertIdentical(delegate.sharerDidFailError as AnyObject, error, .WebDialogDelegate.didFailCalled) XCTAssertIdentical( internalUtility.unregisterTransientObjectObject as AnyObject, dialog, .WebDialogDelegate.unregistersTransientObject ) } func testWebDialogDelegateCompletionWithCancelErrorCode() { dialog = createEmptyDialog(mode: .web) let webDialog = _WebDialog(name: "test") webDialog.delegate = dialog dialog.webDialog = webDialog dialog.webDialog(webDialog, didCompleteWithResults: ["error_code": 4201]) XCTAssertNil(dialog.webDialog, .WebDialogDelegate.clearsWebDialog) XCTAssertTrue(delegate.sharerDidCancelCalled, .WebDialogDelegate.didCancelCalled) XCTAssertIdentical(delegate.sharerDidCancelSharer, dialog, .WebDialogDelegate.didCancelCalled) XCTAssertIdentical( internalUtility.unregisterTransientObjectObject as AnyObject, dialog, .WebDialogDelegate.unregistersTransientObject ) } func testWebDialogDelegateCompletionWithError() throws { dialog = createEmptyDialog(mode: .web) let webDialog = _WebDialog(name: "test") webDialog.delegate = dialog dialog.webDialog = webDialog dialog.webDialog( webDialog, didCompleteWithResults: [ "error_code": 123, "error_message": "message", ] ) XCTAssertNil(dialog.webDialog, .WebDialogDelegate.clearsWebDialog) XCTAssertTrue(delegate.sharerDidFailCalled, .WebDialogDelegate.didFailCalled) XCTAssertIdentical(delegate.sharerDidFailSharer, dialog, .WebDialogDelegate.didFailCalled) let error = try XCTUnwrap(delegate.sharerDidFailError as? TestSDKError, .WebDialogDelegate.didFailCalled) XCTAssertEqual(error.domain, ShareErrorDomain, .WebDialogDelegate.didFailCalled) XCTAssertEqual(error.code, ShareError.unknown.rawValue, .WebDialogDelegate.didFailCalled) XCTAssertEqual(error.userInfo[GraphRequestErrorGraphErrorCodeKey] as? Int, 123, .WebDialogDelegate.didFailCalled) XCTAssertEqual(error.message, "message", .WebDialogDelegate.didFailCalled) XCTAssertNil(error.underlyingError, .WebDialogDelegate.didFailCalled) XCTAssertIdentical( internalUtility.unregisterTransientObjectObject as AnyObject, dialog, .WebDialogDelegate.unregistersTransientObject ) } func testWebDialogDelegateCompletionWithCompletionGestureCancellation() { dialog = createEmptyDialog(mode: .web) let webDialog = _WebDialog(name: "test") webDialog.delegate = dialog dialog.webDialog = webDialog dialog.webDialog( webDialog, didCompleteWithResults: [ ShareBridgeAPI.CompletionGesture.key: ShareBridgeAPI.CompletionGesture.cancelValue, "error_code": 0, ] ) XCTAssertNil(dialog.webDialog, .WebDialogDelegate.clearsWebDialog) XCTAssertTrue(delegate.sharerDidCancelCalled, .WebDialogDelegate.didCancelCalled) XCTAssertIdentical(delegate.sharerDidCancelSharer, dialog, .WebDialogDelegate.didCancelCalled) XCTAssertIdentical( internalUtility.unregisterTransientObjectObject as AnyObject, dialog, .WebDialogDelegate.unregistersTransientObject ) } func testWebDialogDelegateCompletion() throws { dialog = createEmptyDialog(mode: .web) let webDialog = _WebDialog(name: "test") webDialog.delegate = dialog dialog.webDialog = webDialog dialog.webDialog( webDialog, didCompleteWithResults: [ ShareBridgeAPI.PostIDKey.webParameters: "my-post", "error_code": 0, ] ) XCTAssertNil(dialog.webDialog, .WebDialogDelegate.clearsWebDialog) XCTAssertTrue(delegate.sharerDidCompleteCalled, .WebDialogDelegate.didCompleteCalled) XCTAssertIdentical(delegate.sharerDidCompleteSharer, dialog, .WebDialogDelegate.didCompleteCalled) let results = try XCTUnwrap(delegate.sharerDidCompleteResults, .WebDialogDelegate.didCompleteCalled) XCTAssertEqual( results[ShareBridgeAPI.PostIDKey.results] as? String, "my-post", .WebDialogDelegate.didCompleteCalled ) XCTAssertIdentical( internalUtility.unregisterTransientObjectObject as AnyObject, dialog, .WebDialogDelegate.unregistersTransientObject ) } // MARK: - Helpers func createEmptyDialog(mode: ShareDialog.Mode) -> ShareDialog { let dialog = ShareDialog(viewController: nil, content: nil, delegate: delegate) dialog.mode = mode return dialog } func validate( shareContent: SharingContent, expectValid: Bool, expectShow: Bool, mode: ShareDialog.Mode, nonSupportedScheme: String? = nil, file: StaticString = #file, line: UInt = #line ) { internalURLOpener.computeCanOpenURL = { url in url.absoluteString != nonSupportedScheme } let viewController = UIViewController() dialog = createEmptyDialog(mode: mode) dialog.shareContent = shareContent dialog.fromViewController = viewController if expectValid { XCTAssertNoThrow( try dialog.validate(), "Should not throw an error when validating the dialog", file: file, line: line ) } else { XCTAssertThrowsError( try dialog.validate(), "Should not throw an error when validating the dialog" ) } XCTAssertEqual( dialog.show(), expectShow, "Showing the dialog should \(expectShow ? "succeed" : "fail")", file: file, line: line ) } private func validateInitialText( capturedText: String?, expectedAppID: String, expectedHashtag: String, expectedQuotes: [String], file: StaticString = #file, line: UInt = #line ) throws { let capturedComponents = try XCTUnwrap(initialTextComponents(for: capturedText)) XCTAssertEqual( capturedComponents.appID, "fb-app-id:\(expectedAppID)", file: file, line: line ) XCTAssertEqual( capturedComponents.hashtag, expectedHashtag, file: file, line: line ) XCTAssertEqual( capturedComponents.jsonObject.count, 3, file: file, line: line ) XCTAssertEqual( capturedComponents.jsonObject["quotes"] as? [String], expectedQuotes, file: file, line: line ) XCTAssertEqual( capturedComponents.jsonObject["app_id"] as? String, expectedAppID, file: file, line: line ) XCTAssertEqual( capturedComponents.jsonObject["hashtags"] as? [String], [expectedHashtag], file: file, line: line ) } private typealias InitialTextComponents = (appID: String, hashtag: String, jsonObject: [String: Any]) private func initialTextComponents(for potentialText: String?) -> InitialTextComponents? { guard let splitAtVerticalBar = potentialText?.split(separator: "|"), splitAtVerticalBar.count == 2, let beforeJSON = splitAtVerticalBar.first?.split(separator: " "), beforeJSON.count == 2, let appID = beforeJSON.first, let hashtag = beforeJSON.last, let json = splitAtVerticalBar.last, let data = String(json).data(using: .utf8), let jsonObject = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] else { return nil } return ( appID: String(appID), hashtag: String(hashtag), jsonObject: jsonObject ) } } // swiftformat:disable extensionaccesscontrol // MARK: - Assumptions fileprivate extension String { enum DefaultDependencies { static let usesInternalURLOpenerByDefault = """ The default internal URL opening dependency should be the shared UIApplication """ static let usesInternalUtilityByDefault = """ The default internal utility dependency should be the shared InternalUtility """ static let usesSettingsByDefault = "The default settings dependency should be the shared Settings" static let usesShareUtilityByDefault = "The default share utility dependency should be the _ShareUtility class" static let usesShareBridgeAPIRequestFactoryByDefault = """ The default bridge API request factory dependency should be a concrete ShareBridgeAPIRequestFactory """ static let usesBridgeAPIByDefault = """ The default bridge API request opening dependency should be the shared BridgeAPI for its default """ static let usesSocialComposeViewControllerFactoryByDefault = """ The default social compose view controller factory dependency should be a concrete \ SocialComposeViewControllerFactory """ static let usesInternalUtilityAsWindowFinderByDefault = """ The default window finding dependency should be the shared InternalUtility """ static let usesErrorFactoryByDefault = "The default error factory dependency should be a concrete _ErrorFactory" static let usesAppEventsByDefault = "The default event logging dependency should be the shared AppEvents" static let usesPHImageManagerAsMediaLibrarySearcherByDefault = """ The default media library searching dependency should be the default PHImageManager """ } enum CustomDependencies { static let usesCustomInternalURLOpener = "The internal URL opening dependency should be configurable" static let usesCustomInternalUtility = "The internal utility dependency should be configurable" static let usesCustomSettings = "The settings dependency should be configurable" static let usesCustomShareUtility = "The share utility dependency should be configurable" static let usesCustomShareBridgeAPIRequestFactory = """ The bridge API request factory dependency should be configurable """ static let usesCustomBridgeAPIRequestOpener = "The bridge API request opening dependency should be configurable" static let usesCustomSocialComposeViewControllerFactory = """ The social compose view roller factory dependency should be configurable """ static let usesCustomWindowFinder = "The window finding dependency should be configurable" static let usesCustomErrorFactory = "The error factory dependency should be configurable" static let usesCustomEventLogger = "The event logging dependency should be configurable" static let usesCustomMediaLibrarySearcher = "The media library searching dependency should be configurable" } enum Construction { static let createViaClassFactoryMethod = "Can create a dialog with a class factory method" static let createViaClassShowMethod = "Can create and show a dialog with the class `show` method" } enum WebDialogDelegate { static let didCancelCalled = "A dialog invokes its delegate's cancellation method" static let didFailCalled = "A dialog invokes its delegate's failure method" static let didCompleteCalled = "A dialog invokes its delegate's completion method" static let clearsWebDialog = "A dialog clears its web dialog" static let unregistersTransientObject = "A dialog unregisters itself as a transient object" } enum Showing { static let showingRequiresValidContent = "A share dialog will only show with valid share content" static let showsWithValidContent = "A share dialog shows with valid content" static let bridgeAPIRequest = "A bridge API request is created with the appropriate values" static let webShareBridgeComponents = "A bridge API request needs components derived from its content" static let webPhotoContent = "A bridge API request needs values generated for web photo content" } }