diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2c39f0..4ef5a21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,10 +4,12 @@ on: push: branches: - main + - develop - feature/* pull_request: branches: - main + - develop - feature/* workflow_dispatch: @@ -26,6 +28,28 @@ jobs: - name: "Run tests" run: xcodebuild -project "attentive-ios-sdk.xcodeproj" -scheme "attentive-ios-sdk-framework" -destination "platform=iOS Simulator,OS=16.1,name=iPhone 13 Pro" test + ui-tests: + name: Run iOS UI Tests + runs-on: macOS-14 + env: + DEVELOPER_DIR: "/Applications/Xcode_15.3.app/Contents/Developer" + timeout-minutes: 10 + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: "Run UI Tests" + run: | + set -o pipefail + xcodebuild -workspace "attentive-ios-sdk.xcworkspace" -scheme "CreativeUITest" -destination "platform=iOS Simulator,OS=17.4,name=iPhone 15 Pro" -derivedDataPath build/ -resultBundlePath ui-test-results.xcresult test | xcpretty + - name: Compress UI Test Results + if: failure() + run: tar -czf ui-test-results.xcresult.tar.gz ui-test-results.xcresult + - name: Upload UI Test Results + if: failure() + uses: actions/upload-artifact@v3 + with: + name: ui-test-results + path: ui-test-results.xcresult.tar.gz lint: name: Run lint runs-on: ubuntu-20.04 diff --git a/Example/CreativeUITest/CreativeUITest-Bridging-Header.h b/Example/CreativeUITest/CreativeUITest-Bridging-Header.h new file mode 100644 index 0000000..1b2cb5d --- /dev/null +++ b/Example/CreativeUITest/CreativeUITest-Bridging-Header.h @@ -0,0 +1,4 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + diff --git a/Example/CreativeUITest/CreativeUITest.m b/Example/CreativeUITest/CreativeUITest.m index b18744e..71dcb95 100644 --- a/Example/CreativeUITest/CreativeUITest.m +++ b/Example/CreativeUITest/CreativeUITest.m @@ -9,6 +9,7 @@ #import #import #import +#import "CreativeUITest-Swift.h" @interface CreativeUITest : XCTestCase @@ -21,121 +22,127 @@ @implementation CreativeUITest { - (void)setUp { + [super setUp]; // In UI tests it is usually best to stop immediately when a failure occurs. self.continueAfterFailure = NO; - [[self class] clearCookies]; - app = [[XCUIApplication alloc] init]; } -+ (void)tearDown { +- (void)tearDown { // reset cookies & user defaults after all tests have run [self clearCookies]; [self resetUserDefaults]; + + [app terminate]; + [self deleteApp]; + + [super tearDown]; } - (void)testLoadCreative_clickClose_closesCreative { [self launchAppWithMode:@"production"]; - [app.buttons[@"Push me for Creative!"] tap]; + [app.buttons[@"Push me for Creative!"] tapOnElement]; + + sleep(2); // Close the creative - XCTAssertTrue([app.webViews.buttons[@"Dismiss this popup"] waitForExistenceWithTimeout:5.0]); - [app.webViews.buttons[@"Dismiss this popup"] tap]; + [app.webViews.buttons[@"Dismiss this popup"] tapOnElement]; // Assert that the creative is closed - XCTAssertTrue([app.buttons[@"Push me for Creative!"] waitForExistenceWithTimeout:5.0]); + XCTAssertTrue([app.buttons[@"Push me for Creative!"] elementExists]); XCTAssertEqual(app.buttons[@"Push me for Creative!"].isHittable, true); } - (void)testLoadCreative_fillOutFormAndSubmit_launchesSmsAppWithPrePopulatedText { [self launchAppWithMode:@"production"]; - [app.buttons[@"Push me to clear the User!"] tap]; - [app.buttons[@"Push me for Creative!"] tap]; + [app.buttons[@"Push me to clear the User!"] tapOnElement]; + [app.buttons[@"Push me for Creative!"] tapOnElement]; // Fill in the email - XCTAssertTrue([app.webViews.textFields[@"Email Address"] waitForExistenceWithTimeout:5.0]); XCUIElement *emailField = app.webViews.textFields[@"Email Address"]; - [emailField tap]; - [emailField typeText:@"testemail@attentivemobile.com"]; + [emailField tapOnElement]; + [emailField fillTextField:@"testemail@attentivemobile.com"]; // Tap something else on the creative to dismiss the keyboard - [app.staticTexts[@"10% OFF"] tap]; + [app.staticTexts[@"10% OFF"] tapOnElement]; // Submit email - XCTAssertTrue([app.buttons[@"CONTINUE"] waitForExistenceWithTimeout:5.0]); - [app.webViews.buttons[@"CONTINUE"] tap]; + [app.webViews.buttons[@"CONTINUE"] tapOnElement]; - // Click subscribe button - XCTAssertTrue([app.buttons[@"GET 10% OFF NOW when you sign up for email and texts"] waitForExistenceWithTimeout:5.0]); - [app.buttons[@"GET 10% OFF NOW when you sign up for email and texts"] tap]; + [app.buttons[@"GET 10% OFF NOW when you sign up for email and texts"] tapOnElement]; // Assert that the SMS app is opened with prepopulated text if running locally // (AWS Device Farm doesn't allow use of SMS apps) NSString *envHost = [[[NSProcessInfo processInfo] environment] objectForKey:@"COM_ATTENTIVE_EXAMPLE_HOST"]; if ([envHost isEqualToString:@"local"]) { XCUIApplication *smsApp = [[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.MobileSMS"]; - XCTAssertTrue([smsApp.textFields[@"Message"] waitForExistenceWithTimeout:5.0]); + + // Close Info Screen when SMS App is opened for the first time + if ([smsApp.buttons[@"OK"] elementExists]) { + [smsApp.buttons[@"OK"] tapOnElement]; + } + + XCTAssertTrue([smsApp.textFields[@"Message"] elementExists]); XCTAssertTrue([smsApp.textFields[@"Message"].value containsString:@"Send this text to subscribe to recurring automated personalized marketing alerts (e.g. cart reminders) from Attentive Mobile Apps Test"]); + + [app activate]; + sleep(1); } } - (void)testLoadCreative_clickPrivacyLink_opensPrivacyPageInWebApp { [self launchAppWithMode:@"production"]; - [app.buttons[@"Push me for Creative!"] tap]; + [app.buttons[@"Push me for Creative!"] tapOnElement]; // Click privacy link - XCTAssertTrue([app.webViews.links[@"Privacy"] waitForExistenceWithTimeout:5.0]); - [app.webViews.links[@"Privacy"] tap]; - - // Wait for a moment to allow the app to react - sleep(5); - - // Check if the app is no longer active - XCTAssertFalse([app.webViews.links[@"Privacy"] waitForExistenceWithTimeout:5.0]); + [app.links[@"Privacy"] tapOnElement]; // AWS Device Farm doesn't always acknowledge separate apps, leading to flakiness here NSString *envHost = [[[NSProcessInfo processInfo] environment] objectForKey:@"COM_ATTENTIVE_EXAMPLE_HOST"]; if ([envHost isEqualToString:@"local"]) { // Verify that the privacy page is visible in the external browser XCUIApplication *safariApp = [[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.mobilesafari"]; - BOOL privacyPolicyExists = [safariApp.staticTexts[@"Privacy Policy"] waitForExistenceWithTimeout:5.0]; - BOOL messagingPrivacyPolicyExists = [safariApp.staticTexts[@"Messaging Privacy Policy"] waitForExistenceWithTimeout:5.0]; + BOOL privacyPolicyExists = [safariApp.staticTexts[@"Privacy Policy"] elementExists]; + BOOL messagingPrivacyPolicyExists = [safariApp.staticTexts[@"Messaging Privacy Policy"] elementExists]; XCTAssertTrue(privacyPolicyExists || messagingPrivacyPolicyExists); + + [app activate]; + + sleep(1); } } - (void)testLoadCreative_inDebugMode_showsDebugMessage { [self launchAppWithMode:@"debug"]; - [app.buttons[@"Push me for Creative!"] tap]; + [app.buttons[@"Push me for Creative!"] tapOnElement]; // Verify debug page shows - XCTAssertTrue([app.staticTexts[@"Debug output JSON"] waitForExistenceWithTimeout:5.0]); + XCTAssertTrue([app.staticTexts[@"Debug output JSON"] elementExists]); } - (void)testLoadCreative_clickProductPage_closesCreative { [self launchAppWithMode:@"production"]; - [app.buttons[@"Push me for Creative!"] tap]; + [app.buttons[@"Push me for Creative!"] tapOnElement]; // Click privacy link - XCTAssertTrue([app.webViews.links[@"Privacy"] waitForExistenceWithTimeout:5.0]); - [app.tabBars.buttons[@"Product"] tap]; + [app.tabBars.buttons[@"Product"] tapOnElement]; // Verify that the product page is visible - XCTAssertTrue([app.buttons[@"Add To Cart"] waitForExistenceWithTimeout:5.0]); + XCTAssertTrue([app.buttons[@"Add To Cart"] elementExists]); // Nav back, and verify the creative is closed - [app.tabBars.buttons[@"Main"] tap]; - XCTAssertTrue([app.buttons[@"Push me for Creative!"] waitForExistenceWithTimeout:5.0]); + [app.tabBars.buttons[@"Main"] tapOnElement]; + XCTAssertTrue([app.buttons[@"Push me for Creative!"] elementExists]); } -+ (void)clearCookies { +- (void)clearCookies { NSLog(@"Clearing cookies!"); NSSet *websiteDataTypes = [NSSet setWithArray:@[ WKWebsiteDataTypeCookies ]]; NSDate *dateFrom = [NSDate dateWithTimeIntervalSince1970:0]; @@ -147,7 +154,7 @@ + (void)clearCookies { } -+ (void)resetUserDefaults { +- (void)resetUserDefaults { // Reset user defaults for example app, not the test runner [[NSUserDefaults standardUserDefaults] removePersistentDomainForName:@"com.attentive.ExampleTest"]; } diff --git a/Example/CreativeUITest/Extensions/XCTestCase+Extension.swift b/Example/CreativeUITest/Extensions/XCTestCase+Extension.swift new file mode 100644 index 0000000..49cafe9 --- /dev/null +++ b/Example/CreativeUITest/Extensions/XCTestCase+Extension.swift @@ -0,0 +1,24 @@ +// +// XCTestCase+Extension.swift +// CreativeUITest +// +// Created by Vladimir - Work on 2024-05-23. +// + +import XCTest + +extension XCTestCase { + @objc func deleteApp() { + let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") + + let icon = springboard.icons["Example - Local"] + + guard icon.exists else { return } + + icon.press(forDuration: 1) + + springboard.buttons["Remove App"].tapOnElement() + springboard.buttons["Delete App"].tapOnElement() + springboard.buttons["Delete"].tapOnElement() + } +} diff --git a/Example/CreativeUITest/Extensions/XCUIElement+Extension.swift b/Example/CreativeUITest/Extensions/XCUIElement+Extension.swift new file mode 100644 index 0000000..4297b50 --- /dev/null +++ b/Example/CreativeUITest/Extensions/XCUIElement+Extension.swift @@ -0,0 +1,39 @@ +// +// XCUIElement+Extension.swift +// CreativeUITest +// +// Created by Vladimir - Work on 2024-05-22. +// + +import XCTest + +extension XCUIElement { + /// Verify element existence and then proceed with tapping on it + @objc func tapOnElement() { + guard elementExists() else { + XCTFail("\(description) does not exists") + return + } + tap() + } + + /// Verify element existence on the app view hierarchy + @objc func elementExists() -> Bool { + elementExists(timeout: 5) + } + + @objc func fillTextField(_ text: String) { + guard elementExists() else { + XCTFail("\(description) does not exists") + return + } + + typeText(text) + } +} + +fileprivate extension XCUIElement { + func elementExists(timeout: TimeInterval) -> Bool { + waitForExistence(timeout: timeout) + } +} diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index de4bbfb..a5beefd 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -9,7 +9,7 @@ /* Begin PBXBuildFile section */ 5806DE7529774E0700C18FFA /* ProductViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5806DE7429774E0700C18FFA /* ProductViewController.m */; }; 5806DE7629774E0700C18FFA /* ProductViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5806DE7429774E0700C18FFA /* ProductViewController.m */; }; - 58317968292EEEAA0003D6B0 /* BuildFile in Sources */ = {isa = PBXBuildFile; }; + 58317968292EEEAA0003D6B0 /* (null) in Sources */ = {isa = PBXBuildFile; }; 58317969292EEEAA0003D6B0 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 69CCAF5628DCDE57007620BD /* ViewController.m */; }; 5831796A292EEEAA0003D6B0 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 69CCAF5028DCDE57007620BD /* AppDelegate.m */; }; 5831796D292EEEAA0003D6B0 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 69CCAF6128DCDE5A007620BD /* main.m */; }; @@ -19,7 +19,7 @@ 58317974292EEEAA0003D6B0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 69CCAF5828DCDE57007620BD /* Main.storyboard */; }; 588B597529B94AB100E3BA33 /* attentive_ios_sdk_framework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 588B597429B94AB100E3BA33 /* attentive_ios_sdk_framework.framework */; }; 588B597629B94AB100E3BA33 /* attentive_ios_sdk_framework.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 588B597429B94AB100E3BA33 /* attentive_ios_sdk_framework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 58D52E3F292EDA1600CF32DE /* BuildFile in Sources */ = {isa = PBXBuildFile; }; + 58D52E3F292EDA1600CF32DE /* (null) in Sources */ = {isa = PBXBuildFile; }; 69CCAF5128DCDE57007620BD /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 69CCAF5028DCDE57007620BD /* AppDelegate.m */; }; 69CCAF5428DCDE57007620BD /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 69CCAF5328DCDE57007620BD /* SceneDelegate.m */; }; 69CCAF5728DCDE57007620BD /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 69CCAF5628DCDE57007620BD /* ViewController.m */; }; @@ -28,6 +28,8 @@ 69CCAF5F28DCDE5A007620BD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 69CCAF5D28DCDE5A007620BD /* LaunchScreen.storyboard */; }; 69CCAF6228DCDE5A007620BD /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 69CCAF6128DCDE5A007620BD /* main.m */; }; 69D3C14C299EF2D10027934F /* CreativeUITest.m in Sources */ = {isa = PBXBuildFile; fileRef = 69D3C14B299EF2D10027934F /* CreativeUITest.m */; }; + FB51061A2BFE4A9700D9A72D /* XCUIElement+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB5106192BFE4A9700D9A72D /* XCUIElement+Extension.swift */; }; + FBA017C92BFFE54400CB2969 /* XCTestCase+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBA017C82BFFE54400CB2969 /* XCTestCase+Extension.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -76,6 +78,9 @@ 69CCAF6128DCDE5A007620BD /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 69D3C149299EF2D10027934F /* CreativeUITest.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CreativeUITest.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 69D3C14B299EF2D10027934F /* CreativeUITest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CreativeUITest.m; sourceTree = ""; }; + FB5106182BFE4A9700D9A72D /* CreativeUITest-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CreativeUITest-Bridging-Header.h"; sourceTree = ""; }; + FB5106192BFE4A9700D9A72D /* XCUIElement+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCUIElement+Extension.swift"; sourceTree = ""; }; + FBA017C82BFFE54400CB2969 /* XCTestCase+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XCTestCase+Extension.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -167,7 +172,9 @@ 69D3C14A299EF2D10027934F /* CreativeUITest */ = { isa = PBXGroup; children = ( + FBA017CA2BFFE70500CB2969 /* Extensions */, 69D3C14B299EF2D10027934F /* CreativeUITest.m */, + FB5106182BFE4A9700D9A72D /* CreativeUITest-Bridging-Header.h */, ); path = CreativeUITest; sourceTree = ""; @@ -179,6 +186,15 @@ path = Pods; sourceTree = ""; }; + FBA017CA2BFFE70500CB2969 /* Extensions */ = { + isa = PBXGroup; + children = ( + FB5106192BFE4A9700D9A72D /* XCUIElement+Extension.swift */, + FBA017C82BFFE54400CB2969 /* XCTestCase+Extension.swift */, + ); + path = Extensions; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -249,6 +265,7 @@ }; 69D3C148299EF2D10027934F = { CreatedOnToolsVersion = 14.2; + LastSwiftMigration = 1530; TestTargetID = 58317964292EEEAA0003D6B0; }; }; @@ -309,7 +326,7 @@ buildActionMask = 2147483647; files = ( 5806DE7629774E0700C18FFA /* ProductViewController.m in Sources */, - 58317968292EEEAA0003D6B0 /* BuildFile in Sources */, + 58317968292EEEAA0003D6B0 /* (null) in Sources */, 58317969292EEEAA0003D6B0 /* ViewController.m in Sources */, 5831796A292EEEAA0003D6B0 /* AppDelegate.m in Sources */, 5831796D292EEEAA0003D6B0 /* main.m in Sources */, @@ -322,7 +339,7 @@ buildActionMask = 2147483647; files = ( 5806DE7529774E0700C18FFA /* ProductViewController.m in Sources */, - 58D52E3F292EDA1600CF32DE /* BuildFile in Sources */, + 58D52E3F292EDA1600CF32DE /* (null) in Sources */, 69CCAF5728DCDE57007620BD /* ViewController.m in Sources */, 69CCAF5128DCDE57007620BD /* AppDelegate.m in Sources */, 69CCAF6228DCDE5A007620BD /* main.m in Sources */, @@ -334,7 +351,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + FBA017C92BFFE54400CB2969 /* XCTestCase+Extension.swift in Sources */, 69D3C14C299EF2D10027934F /* CreativeUITest.m in Sources */, + FB51061A2BFE4A9700D9A72D /* XCUIElement+Extension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -604,6 +623,7 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; @@ -632,6 +652,9 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OBJC_BRIDGING_HEADER = "CreativeUITest/CreativeUITest-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = "Example - Local"; }; @@ -641,6 +664,7 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; @@ -669,6 +693,8 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OBJC_BRIDGING_HEADER = "CreativeUITest/CreativeUITest-Bridging-Header.h"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = "Example - Local"; }; diff --git a/Example/Example.xcodeproj/xcshareddata/xcschemes/CreativeUITest.xcscheme b/Example/Example.xcodeproj/xcshareddata/xcschemes/CreativeUITest.xcscheme new file mode 100644 index 0000000..c977179 --- /dev/null +++ b/Example/Example.xcodeproj/xcshareddata/xcschemes/CreativeUITest.xcscheme @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + +