feat: add snapshot UITest and document

This commit is contained in:
CMK 2022-03-03 19:51:12 +08:00
parent 37f4bc1fc9
commit f2f71e7102
5 changed files with 306 additions and 33 deletions

View File

@ -14,7 +14,7 @@
"testTargets" : [
{
"selectedTests" : [
"MastodonUISnapshotTests\/testSnapshot()"
"MastodonUISnapshotTests\/testSmoke()"
],
"target" : {
"containerPath" : "container:Mastodon.xcodeproj",

79
Documentation/Snapshot.md Normal file
View File

@ -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 `<Email>` and `<Password>` for test account.
```zsh
# build and run test case for auto sign-in
TEST_RUNNER_email='<Email>' \
TEST_RUNNER_password='<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 <path_for_xcresult> <path_for_destination>` extracts snapshots.
```zsh
# scresult path for previous test case
xcparse screenshots '<path_for_xcresult>' ~/Downloads/MastodonBuild/Screenshots/
# output
100% [============]
🎊 Export complete! 🎊
```

View File

@ -7,7 +7,7 @@
<key>AppShared.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>21</integer>
<integer>20</integer>
</dict>
<key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict>
@ -102,7 +102,7 @@
<key>MastodonIntent.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>23</integer>
<integer>21</integer>
</dict>
<key>MastodonIntents.xcscheme_^#shared#^_</key>
<dict>
@ -122,7 +122,7 @@
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>24</integer>
<integer>19</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>

View File

@ -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
}()

View File

@ -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)
}