diff --git a/AppStoreSnapshotTestPlan.xctestplan b/AppStoreSnapshotTestPlan.xctestplan index ebe40293..02e1644e 100644 --- a/AppStoreSnapshotTestPlan.xctestplan +++ b/AppStoreSnapshotTestPlan.xctestplan @@ -14,7 +14,7 @@ "testTargets" : [ { "selectedTests" : [ - "MastodonUISnapshotTests\/testSnapshot()" + "MastodonUISnapshotTests\/testSmoke()" ], "target" : { "containerPath" : "container:Mastodon.xcodeproj", diff --git a/Documentation/Snapshot.md b/Documentation/Snapshot.md new file mode 100644 index 00000000..9f20abbd --- /dev/null +++ b/Documentation/Snapshot.md @@ -0,0 +1,79 @@ +# Mastodon App Store Snapshot Guide +This documentation is a guide to create snapshots for App Store. The outer contributor could ignore this. + +## Prepare toolkit +The app use the Xcode UITest generate snapshots attachments. Then use the `xcparse` tool extract the snapshots. + +```zsh +# install xcparse from Homebrew +brew install chargepoint/xcparse/xcparse +``` +## Take Snapshots +We use `xcodebuild` CLI tool to trigger UITest. To change device for snapshot. + +Replace the `name` in `-destinatio` option to change device. For example: +`-destination 'platform=iOS Simulator,name=iPad Pro (12.9-inch) (5th generation)' \` + +```zsh +# list the destinations +xcodebuild \ + test \ + -showdestinations \ + -derivedDataPath '~/Downloads/MastodonBuild/Derived' \ + -workspace Mastodon.xcworkspace \ + -scheme 'Mastodon - Snapshot' +``` + +#### Auto-Login before make snapshots +This script trigger the `MastodonUITests/MastodonUISnapshotTests/testSignInAccount` test case to sign-in the account. The test case may wait for 2FA code or email code. Please input it if needed. Also, you can skip this and sign-in the test account manually. + +Replace the `` and `` for test account. +```zsh +# build and run test case for auto sign-in +TEST_RUNNER_email='' \ + TEST_RUNNER_password='' \ + xcodebuild \ + test \ + -derivedDataPath '~/Downloads/MastodonBuild/Derived' \ + -workspace Mastodon.xcworkspace \ + -scheme 'Mastodon - Snapshot' \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 13 Pro Max' \ + -testPlan 'AppStoreSnapshotTestPlan' \ + -only-testing:MastodonUITests/MastodonUISnapshotTests/testSignInAccount +``` + +Note: +UITest may running silent. Open the Simulator.app to make the device display. + +#### Take and extract snapshots +```zsh +# take snapshots +TEST_RUNNER_username_snapshot='Gargron' \ + xcodebuild \ + test \ + -derivedDataPath '~/Downloads/MastodonBuild/Derived' \ + -workspace Mastodon.xcworkspace \ + -scheme 'Mastodon - Snapshot' \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 13 Pro Max' \ + -testPlan 'AppStoreSnapshotTestPlan' \ + -only-testing:MastodonUITests/MastodonUISnapshotTests/testSnapshot + +# output: +Test session results, code coverage, and logs: + /Users/Me/Downloads/MastodonBuild/Derived/Logs/Test/Test-Mastodon - Snapshot-2022.03.03_18-00-38-+0800.xcresult + +** TEST SUCCEEDED ** +``` + +Use `xcparse screenshots ` extracts snapshots. + +```zsh +# scresult path for previous test case +xcparse screenshots '' ~/Downloads/MastodonBuild/Screenshots/ + +# output +100% [============] +🎊 Export complete! 🎊 +``` diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 73e417c8..a5d4f6d9 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ AppShared.xcscheme_^#shared#^_ orderHint - 21 + 20 CoreDataStack.xcscheme_^#shared#^_ @@ -102,7 +102,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 23 + 21 MastodonIntents.xcscheme_^#shared#^_ @@ -122,7 +122,7 @@ ShareActionExtension.xcscheme_^#shared#^_ orderHint - 24 + 19 SuppressBuildableAutocreation diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index fda5a471..d49235c8 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -51,6 +51,7 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media let barButtonItem = UIBarButtonItem() barButtonItem.tintColor = ThemeService.tintColor barButtonItem.image = UIImage(systemName: "gear")?.withRenderingMode(.alwaysTemplate) + barButtonItem.accessibilityLabel = L10n.Common.Controls.Actions.settings return barButtonItem }() @@ -58,6 +59,7 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media let barButtonItem = UIBarButtonItem() barButtonItem.tintColor = ThemeService.tintColor barButtonItem.image = UIImage(systemName: "square.and.pencil")?.withRenderingMode(.alwaysTemplate) + barButtonItem.accessibilityLabel = L10n.Common.Controls.Actions.compose return barButtonItem }() diff --git a/MastodonUITests/MastodonUISnapshotTests.swift b/MastodonUITests/MastodonUISnapshotTests.swift index 632beb4d..aab493cf 100644 --- a/MastodonUITests/MastodonUISnapshotTests.swift +++ b/MastodonUITests/MastodonUISnapshotTests.swift @@ -38,81 +38,79 @@ extension MastodonUISnapshotTests { // Any test you write for XCTest can be annotated as throws and async. // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - - + } } extension MastodonUISnapshotTests { + + private func tapTab(app: XCUIApplication, tab: String) { + let searchTab = app.tabBars.buttons[tab] + if searchTab.exists { searchTab.tap() } + + let searchCell = app.collectionViews.cells[tab] + if searchCell.exists { searchCell.tap() } + } func testSnapshot() async throws { let app = XCUIApplication() app.launch() - try await snapshotHome() - try await snapshotSearch() - try await snapshotProfile() - + try await testSnapshotHome() + try await testSnapshotSearch() + try await testSnapshotProfile() + try await testSnapshotCompose() } - func snapshotHome() async throws { + func testSnapshotHome() async throws { let app = XCUIApplication() app.launch() - func tapTab() { - XCTAssert(app.tabBars.buttons["Home"].exists) - app.tabBars.buttons["Home"].tap() - } - - tapTab() + tapTab(app: app, tab: "Home") try await Task.sleep(nanoseconds: .second * 3) takeSnapshot(name: "Home - 1") - tapTab() + tapTab(app: app, tab: "Home") try await Task.sleep(nanoseconds: .second * 3) takeSnapshot(name: "Home - 2") - tapTab() + tapTab(app: app, tab: "Home") try await Task.sleep(nanoseconds: .second * 3) takeSnapshot(name: "Home - 3") } - func snapshotSearch() async throws { + func testSnapshotSearch() async throws { let app = XCUIApplication() app.launch() - func tapTab() { - XCTAssert(app.tabBars.buttons["Search"].exists) - app.tabBars.buttons["Search"].tap() - } - - tapTab() + tapTab(app: app, tab: "Search") try await Task.sleep(nanoseconds: .second * 3) takeSnapshot(name: "Search - 1") - tapTab() + tapTab(app: app, tab: "Search") try await Task.sleep(nanoseconds: .second * 3) takeSnapshot(name: "Search - 2") - tapTab() + tapTab(app: app, tab: "Search") try await Task.sleep(nanoseconds: .second * 3) takeSnapshot(name: "Search - 3") } - func snapshotProfile() async throws { + func testSnapshotProfile() async throws { + let username = ProcessInfo.processInfo.environment["username_snapshot"] ?? "Gargron" + let app = XCUIApplication() app.launch() // Go to Search tab - XCTAssert(app.tabBars.buttons["Search"].exists) - app.tabBars.buttons["Search"].tap() + tapTab(app: app, tab: "Search") // Tap and search user let searchField = app.navigationBars.searchFields.firstMatch XCTAssert(searchField.waitForExistence(timeout: 5)) searchField.tap() - searchField.typeText("@dentaku@fnordon.de") + searchField.typeText(username) // Tap the cell and display user profile let cell = app.tables.cells.firstMatch @@ -124,12 +122,206 @@ extension MastodonUISnapshotTests { takeSnapshot(name: "Profile") } + func testSnapshotCompose() async throws { + let app = XCUIApplication() + app.launch() + + // open Compose scene + let composeBarButtonItem = app.navigationBars.buttons["Compose"].firstMatch + let composeCollectionViewCell = app.collectionViews.cells["Compose"] + if composeBarButtonItem.waitForExistence(timeout: 5) { + composeBarButtonItem.tap() + } else if composeCollectionViewCell.waitForExistence(timeout: 5) { + composeCollectionViewCell.tap() + } else { + XCTFail() + } + + // type text + let textView = app.textViews.firstMatch + XCTAssert(textView.waitForExistence(timeout: 5)) + textView.tap() + textView.typeText("Look at that view! #Athens ") + + // tap Add Attachment toolbar button + let addAttachmentButton = app.buttons["Add Attachment"].firstMatch + XCTAssert(addAttachmentButton.waitForExistence(timeout: 5)) + addAttachmentButton.tap() + + // tap Photo Library menu action + let photoLibraryButton = app.buttons["Photo Library"].firstMatch + XCTAssert(photoLibraryButton.waitForExistence(timeout: 5)) + photoLibraryButton.tap() + + // select photo + let photo = app.images["Photo, August 09, 2012, 2:52 AM"].firstMatch + XCTAssert(photo.waitForExistence(timeout: 5)) + photo.tap() + + // tap Add barButtonItem + let addBarButtonItem = app.navigationBars.buttons["Add"].firstMatch + XCTAssert(addBarButtonItem.waitForExistence(timeout: 5)) + addBarButtonItem.tap() + + try await Task.sleep(nanoseconds: .second * 10) + takeSnapshot(name: "Compose - 1") + + try await Task.sleep(nanoseconds: .second * 10) + takeSnapshot(name: "Compose - 2") + + try await Task.sleep(nanoseconds: .second * 10) + takeSnapshot(name: "Compose - 3") + } + +} + +extension MastodonUISnapshotTests { + + // Please check the Documentation/Snapshot.md and run this test case in the command line + func testSignInAccount() async throws { + guard let email = ProcessInfo.processInfo.environment["email"] else { + fatalError("env 'email' missing") + } + guard let password = ProcessInfo.processInfo.environment["password"] else { + fatalError("env 'password' missing") + } + try await signInApplication(email: email, password: password) + } + + func signInApplication( + email: String, + password: String + ) async throws { + let app = XCUIApplication() + app.launch() + + // check in Onboarding or not + let loginButton = app.buttons["Log In"].firstMatch + let loginButtonExists = loginButton.waitForExistence(timeout: 5) + + // goto Onboarding scene if already sign-in + if !loginButtonExists { + let profileTabBarButton = app.tabBars.buttons["Profile"] + XCTAssert(profileTabBarButton.waitForExistence(timeout: 3)) + profileTabBarButton.press(forDuration: 2) + + let addAccountCell = app.cells.containing(.staticText, identifier: "Add Account").firstMatch + XCTAssert(addAccountCell.waitForExistence(timeout: 3)) + addAccountCell.tap() + } + + // Tap login button + XCTAssert(loginButtonExists) + loginButton.tap() + + // type domain + let domainTextField = app.textFields.firstMatch + XCTAssert(domainTextField.waitForExistence(timeout: 5)) + domainTextField.tap() + // Skip system keyboard swipe input guide + skipKeyboardSwipeInputGuide(app: app) + domainTextField.typeText("mastodon.social") + XCUIApplication().keyboards.buttons["Done"].firstMatch.tap() + + // wait searching + try await Task.sleep(nanoseconds: .second * 3) + + // tap server + let cell = app.cells.containing(.staticText, identifier: "mastodon.social").firstMatch + XCTAssert(cell.waitForExistence(timeout: 5)) + cell.tap() + + // add system alert monitor + // A. The monitor not works + // addUIInterruptionMonitor(withDescription: "Authentication Alert") { alert in + // alert.buttons["Continue"].firstMatch.tap() + // return true + // } + + // tap next button + let nextButton = app.buttons.matching(NSPredicate(format: "enabled == true")).matching(identifier: "Next").firstMatch + XCTAssert(nextButton.waitForExistence(timeout: 3)) + nextButton.tap() + + // wait authentication alert display + try await Task.sleep(nanoseconds: .second * 3) + + // B. Workaround + let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard") + let continueButton = springboard.buttons["Continue"].firstMatch + XCTAssert(continueButton.waitForExistence(timeout: 3)) + continueButton.tap() + + // wait OAuth webpage display + try await Task.sleep(nanoseconds: .second * 10) + + let webview = app.webViews.firstMatch + XCTAssert(webview.waitForExistence(timeout: 10)) + + func tapAuthorizeButton() async throws -> Bool { + let authorizeButton = webview.buttons["AUTHORIZE"].firstMatch + if authorizeButton.exists { + authorizeButton.tap() + try await Task.sleep(nanoseconds: .second * 5) + return true + } + return false + } + + let isAuthorized = try await tapAuthorizeButton() + if !isAuthorized { + let emailTextField = webview.textFields["E-mail address"].firstMatch + XCTAssert(emailTextField.waitForExistence(timeout: 10)) + emailTextField.tap() + emailTextField.typeText(email) + + let passwordTextField = webview.secureTextFields["Password"].firstMatch + XCTAssert(passwordTextField.waitForExistence(timeout: 3)) + passwordTextField.tap() + passwordTextField.typeText(password) + + let goKeyboardButton = XCUIApplication().keyboards.buttons["Go"].firstMatch + XCTAssert(goKeyboardButton.waitForExistence(timeout: 3)) + goKeyboardButton.tap() + + var retry = 0 + let retryLimit = 20 + while webview.exists { + guard retry < retryLimit else { + fatalError("Cannot complete OAuth process") + } + retry += 1 + + // will break due to webview dismiss + _ = try await tapAuthorizeButton() + + print("Please enter the sign-in confirm code. Retry in 5s") + try await Task.sleep(nanoseconds: .second * 5) + } + } else { + // Done + } + + print("OAuth finish") + } + + private func skipKeyboardSwipeInputGuide(app: XCUIApplication) { + let swipeInputLabel = app.staticTexts["Speed up your typing by sliding your finger across the letters to compose a word."].firstMatch + guard swipeInputLabel.waitForExistence(timeout: 3) else { return } + let continueButton = app.buttons["Continue"] + continueButton.tap() + } } extension MastodonUISnapshotTests { func takeSnapshot(name: String) { let snapshot = XCUIScreen.main.screenshot() - let attachment = XCTAttachment(screenshot: snapshot) + let attachment = XCTAttachment( + uniformTypeIdentifier: "public.png", + name: "Screenshot-\(name)-\(UIDevice.current.name).png", + payload: snapshot.pngRepresentation, + userInfo: nil + ) attachment.lifetime = .keepAlways add(attachment) }