forked from zelo72/mastodon-ios
feat: add snapshot UITest and document
This commit is contained in:
parent
37f4bc1fc9
commit
f2f71e7102
|
@ -14,7 +14,7 @@
|
|||
"testTargets" : [
|
||||
{
|
||||
"selectedTests" : [
|
||||
"MastodonUISnapshotTests\/testSnapshot()"
|
||||
"MastodonUISnapshotTests\/testSmoke()"
|
||||
],
|
||||
"target" : {
|
||||
"containerPath" : "container:Mastodon.xcodeproj",
|
||||
|
|
|
@ -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! 🎊
|
||||
```
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}()
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue