From 1eca29373a5c27e7dfe7871be77be306ca626989 Mon Sep 17 00:00:00 2001
From: Nikolai Madlener <33159293+NikolaiMadlener@users.noreply.github.com>
Date: Mon, 16 Oct 2023 14:37:14 +0200
Subject: [PATCH] Add Customizable Area to AccountOverview (#27)
# Add Customizable Area to AccountOverview
## :recycle: Current situation & Problem
Like described in issue #25, some applications like the Spezi Template
application could display some additional information such as licensing
information or other settings at the bottom of the Account Overview.
Furthermore, some developers might want to do it the other way around
and link to account information within a settings page.
## :bulb: Proposed Solution
Add footer components to AccountOverview, as suggested by @PSchmiedmayer
Implement an additional "AccountHeader" that looks something like the
one in the Apple Settings app, as suggested by @Supereg
## :gear: Release Notes
This PR adds two major things.
1. A customizable content section ViewBuilder to the `AccountOverview`.
Simply pass any view as a trailing closure of AccountOverview and it
will be rendered as a Section right between the other AccountOverview
information and the log out button. Here is an example:
```
AccountOverview() {
NavigationLink {
// ...
} label: {
Text("General Settings")
}
NavigationLink {
// ...
} label: {
Text("License Information")
}
}
```
2. A `AccountHeader`, similar to the one of the iOS Settings App, that
could be used as a button that brings a user to the `AccountOverview`.
Here is an example:
```
Section {
NavigationLink {
AccountOverview()
} label: {
AccountHeader(details: details)
}
}
```
## :books: Documentation
*in progress*
## :white_check_mark: Testing
UI tests for the customizable content section have been written.
## :pencil: Code of Conduct & Contributing Guidelines
By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [x] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
---------
Co-authored-by: Paul Schmiedmayer
---
.github/workflows/build-and-test.yml | 6 -
CONTRIBUTORS.md | 1 +
Sources/SpeziAccount/AccountHeader.swift | 106 ++++++++++++++++
Sources/SpeziAccount/AccountOverview.swift | 61 ++++++++--
.../Resources/de.lproj/Localizable.strings | 3 +
.../Resources/en.lproj/Localizable.strings | 3 +
.../AccountOverviewSections.swift | 113 ++++++++++--------
.../AccountOverview/PasswordChangeSheet.swift | 1 -
.../Views/AccountSummaryBox.swift | 1 +
.../AccountTests/AccountTestsView.swift | 61 +++++-----
.../TestAppUITests/AccountOverviewTests.swift | 11 ++
11 files changed, 274 insertions(+), 93 deletions(-)
create mode 100644 Sources/SpeziAccount/AccountHeader.swift
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 7abcf33f..365e9889 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -23,12 +23,6 @@ jobs:
artifactname: SpeziAccount.xcresult
runsonlabels: '["macOS", "self-hosted"]'
scheme: SpeziAccount
- build:
- name: Build Swift Package on Xcode 14
- uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
- with:
- runsonlabels: '["macos-13"]'
- scheme: SpeziAccount
buildandtestuitests:
name: Build and Test UI Tests
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 90beb818..4e5026c1 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -13,3 +13,4 @@ Spezi Account Contributors
* [Paul Schmiedmayer](https://github.com/PSchmiedmayer)
* [Andreas Bauer](https://github.com/Supereg)
+* [Nikolai Madlener](https://github.com/NikolaiMadlener)
diff --git a/Sources/SpeziAccount/AccountHeader.swift b/Sources/SpeziAccount/AccountHeader.swift
new file mode 100644
index 00000000..b5d20b28
--- /dev/null
+++ b/Sources/SpeziAccount/AccountHeader.swift
@@ -0,0 +1,106 @@
+//
+// This source file is part of the Stanford Spezi open-source project
+//
+// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md)
+//
+// SPDX-License-Identifier: MIT
+//
+
+import SpeziViews
+import SwiftUI
+
+
+/// A summary view for ``SpeziAccountOverview`` that can be used as a Button to link to ``SpeziAccountOverview``.
+///
+/// Below is a short code example on how to use the `AccountHeader` view.
+///
+/// ```swift
+/// struct MyView: View {
+/// var body: some View {
+/// NavigationStack {
+/// Form {
+/// Section {
+/// NavigationLink {
+/// AccountOverview()
+/// } label: {
+/// AccountHeader(details: details)
+/// }
+/// }
+/// }
+/// }
+/// }
+/// }
+/// ```
+public struct AccountHeader: View {
+ /// Default values for the ``AccountHeader`` view.
+ @_documentation(visibility: internal)
+ public enum Defaults {
+ /// Default caption.
+ @_documentation(visibility: internal)
+ public static let caption = LocalizedStringResource("ACCOUNT_HEADER_CAPTION", bundle: .atURL(from: .module)) // swiftlint:disable:this attributes
+ }
+
+ @EnvironmentObject private var account: Account
+ private var caption: LocalizedStringResource
+
+ public var body: some View {
+ let accountDetails = account.details
+
+ HStack {
+ UserProfileView(name: accountDetails?.name ?? PersonNameComponents(givenName: "Placeholder", familyName: "Placeholder"))
+ .frame(height: 60)
+ .redacted(reason: account.details == nil ? .placeholder : [])
+ .accessibilityHidden(true)
+ VStack(alignment: .leading) {
+ Text(accountDetails?.name?.formatted() ?? "Placeholder")
+ .font(.title2)
+ .fontWeight(.semibold)
+ .redacted(reason: account.details == nil ? .placeholder : [])
+ Text(caption)
+ .font(.caption)
+ }
+ }
+ }
+
+ /// Display a new Account Header.
+ /// - Parameter caption: A descriptive text displayed under the account name giving the user a brief explanation of what to expect when they interact with the header.
+ public init(caption: LocalizedStringResource = Defaults.caption) {
+ self.caption = caption
+ }
+}
+
+
+#if DEBUG
+#Preview {
+ let details = AccountDetails.Builder()
+ .set(\.userId, value: "andi.bauer@tum.de")
+ .set(\.name, value: PersonNameComponents(givenName: "Andreas", familyName: "Bauer"))
+
+ return AccountHeader()
+ .environmentObject(Account(building: details, active: MockUserIdPasswordAccountService()))
+}
+
+#Preview {
+ AccountHeader(caption: "Email, Password, Preferences")
+ .environmentObject(Account(MockUserIdPasswordAccountService()))
+}
+
+#Preview {
+ let details = AccountDetails.Builder()
+ .set(\.userId, value: "andi.bauer@tum.de")
+ .set(\.name, value: PersonNameComponents(givenName: "Andreas", familyName: "Bauer"))
+
+ return NavigationStack {
+ Form {
+ Section {
+ NavigationLink {
+ AccountOverview()
+ } label: {
+ AccountHeader()
+ }
+ }
+ }
+ }
+ .environmentObject(Account(building: details, active: MockUserIdPasswordAccountService()))
+}
+#endif
diff --git a/Sources/SpeziAccount/AccountOverview.swift b/Sources/SpeziAccount/AccountOverview.swift
index a4924c26..a71749b2 100644
--- a/Sources/SpeziAccount/AccountOverview.swift
+++ b/Sources/SpeziAccount/AccountOverview.swift
@@ -33,16 +33,35 @@ import SwiftUI
/// AccountOverview()
/// }
/// }
+/// ```
+///
+/// Optionally, additional sections can be passed to AccountOverview within the trailing closure, providing the opportunity for customization and extension of the view."
+/// Below is a short code example.
+///
+/// ```swift
+/// struct MyView: View {
+/// var body: some View {
+/// AccountOverview {
+/// NavigationLink {
+/// // ... next view
+/// } label: {
+/// Text("General Settings")
+/// }
+/// }
+/// }
+/// }
+/// ```
///
/// - Note: The ``init(isEditing:)`` initializer allows to pass an optional `Bool` Binding to retrieve the
/// current edit mode of the view. This can be helpful to, e.g., render a custom `Close` Button if the
/// view is not editing when presenting the AccountOverview in a sheet.
-/// ```
-public struct AccountOverview: View {
+public struct AccountOverview: View {
@EnvironmentObject private var account: Account
-
+
@Binding private var isEditing: Bool
-
+
+ let additionalSections: AdditionalSections
+
public var body: some View {
VStack {
if let details = account.details {
@@ -53,7 +72,9 @@ public struct AccountOverview: View {
account: account,
details: details,
isEditing: $isEditing
- )
+ ) {
+ additionalSections
+ }
}
.padding(.top, -20)
} else {
@@ -68,12 +89,15 @@ public struct AccountOverview: View {
.navigationTitle(Text("ACCOUNT_OVERVIEW", bundle: .module))
.navigationBarTitleDisplayMode(.inline)
}
-
-
+
+
/// Display a new Account Overview.
- /// - Parameter isEditing: A Binding that allows you to read the current editing state of the Account Overview view.
- public init(isEditing: Binding = .constant(false)) {
+ /// - Parameters:
+ /// - isEditing: A Binding that allows you to read the current editing state of the Account Overview view.
+ /// - additionalSections: Optional additional sections displayed between the other AccountOverview information and the log out button.
+ public init(isEditing: Binding = .constant(false), @ViewBuilder additionalSections: () -> AdditionalSections = { EmptyView() }) {
self._isEditing = isEditing
+ self.additionalSections = additionalSections()
}
}
@@ -84,13 +108,26 @@ struct AccountOverView_Previews: PreviewProvider {
.set(\.userId, value: "andi.bauer@tum.de")
.set(\.name, value: PersonNameComponents(givenName: "Andreas", familyName: "Bauer"))
.set(\.genderIdentity, value: .male)
-
+
static var previews: some View {
NavigationStack {
- AccountOverview()
+ AccountOverview {
+ NavigationLink {
+ Text("")
+ .navigationTitle(Text("Settings"))
+ } label: {
+ Text("General Settings")
+ }
+ NavigationLink {
+ Text("")
+ .navigationTitle(Text("Package Dependencies"))
+ } label: {
+ Text("License Information")
+ }
+ }
}
.environmentObject(Account(building: details, active: MockUserIdPasswordAccountService()))
-
+
NavigationStack {
AccountOverview()
}
diff --git a/Sources/SpeziAccount/Resources/de.lproj/Localizable.strings b/Sources/SpeziAccount/Resources/de.lproj/Localizable.strings
index 6c5547ca..a107688a 100644
--- a/Sources/SpeziAccount/Resources/de.lproj/Localizable.strings
+++ b/Sources/SpeziAccount/Resources/de.lproj/Localizable.strings
@@ -70,6 +70,9 @@
"VALUE_ADD %@" = "%@ Hinzufügen";
"CHANGE_PASSWORD" = "Passwort Ändern";
+// MARK - Account Header
+"ACCOUNT_HEADER_CAPTION" = "Kontoinformationen & Details";
+
// MARK - Confirmation Dialogs
"CONFIRMATION_DISCARD_CHANGES_TITLE" = "Willst du alle Änderungen verwerfen?";
"CONFIRMATION_DISCARD_INPUT_TITLE" = "Willst du deine Eingaben verwerfen?";
diff --git a/Sources/SpeziAccount/Resources/en.lproj/Localizable.strings b/Sources/SpeziAccount/Resources/en.lproj/Localizable.strings
index c23c7955..161d19f8 100644
--- a/Sources/SpeziAccount/Resources/en.lproj/Localizable.strings
+++ b/Sources/SpeziAccount/Resources/en.lproj/Localizable.strings
@@ -70,6 +70,9 @@
"VALUE_ADD %@" = "Add %@";
"CHANGE_PASSWORD" = "Change Password";
+// MARK - Account Header
+"ACCOUNT_HEADER_CAPTION" = "Account Information & Details";
+
// MARK - Confirmation Dialogs
"CONFIRMATION_DISCARD_CHANGES_TITLE" = "Are you sure you want to discard your changes?";
"CONFIRMATION_DISCARD_INPUT_TITLE" = "Are you sure you want to discard your input?";
diff --git a/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewSections.swift b/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewSections.swift
index be710f4e..e6268893 100644
--- a/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewSections.swift
+++ b/Sources/SpeziAccount/Views/AccountOverview/AccountOverviewSections.swift
@@ -12,33 +12,34 @@ import SwiftUI
/// A internal subview of ``AccountOverview`` that expects to be embedded into a `Form`.
-struct AccountOverviewSections: View {
+struct AccountOverviewSections: View {
+ let additionalSections: AdditionalSections
private let accountDetails: AccountDetails
-
+
private var service: any AccountService {
accountDetails.accountService
}
-
+
@EnvironmentObject private var account: Account
-
+
@Environment(\.logger) private var logger
@Environment(\.editMode) private var editMode
@Environment(\.dismiss) private var dismiss
-
+
@StateObject private var model: AccountOverviewFormViewModel
@Binding private var isEditing: Bool
-
+
@State private var viewState: ViewState = .idle
// separate view state for any destructive actions like logout or account removal
@State private var destructiveViewState: ViewState = .idle
@FocusState private var focusedDataEntry: String? // see `AccountKey.Type/focusState`
-
-
+
+
var isProcessing: Bool {
viewState == .processing || destructiveViewState == .processing
}
-
-
+
+
var body: some View {
AccountOverviewHeader(details: accountDetails)
// Every `Section` is basically a `Group` view. So we have to be careful where to place modifiers
@@ -95,8 +96,8 @@ struct AccountOverviewSections: View {
}) {
Text("UP_LOGOUT", bundle: .module)
}
- .environment(\.defaultErrorDescription, .init("UP_LOGOUT_FAILED_DEFAULT_ERROR", bundle: .atURL(from: .module)))
-
+ .environment(\.defaultErrorDescription, .init("UP_LOGOUT_FAILED_DEFAULT_ERROR", bundle: .atURL(from: .module)))
+
Button(role: .cancel, action: {}) {
Text("CANCEL", bundle: .module)
}
@@ -110,7 +111,7 @@ struct AccountOverviewSections: View {
Text("DELETE", bundle: .module)
}
.environment(\.defaultErrorDescription, .init("REMOVE_DEFAULT_ERROR", bundle: .atURL(from: .module)))
-
+
Button(role: .cancel, action: {}) {
Text("CANCEL", bundle: .module)
}
@@ -121,7 +122,7 @@ struct AccountOverviewSections: View {
// sync the edit mode with the outer view
isEditing = newValue
}
-
+
Section {
NavigationLink {
NameOverview(model: model, details: accountDetails)
@@ -134,31 +135,35 @@ struct AccountOverviewSections: View {
model.accountSecurityLabel(account.configuration)
}
}
-
+
sectionsView
.injectEnvironmentObjects(service: service, model: model, focusState: $focusedDataEntry)
.animation(nil, value: editMode?.wrappedValue)
-
- HStack {
- if editMode?.wrappedValue.isEditing == true {
- AsyncButton(role: .destructive, state: $destructiveViewState, action: {
- // While the action closure itself is not async, we rely on ability to render loading indicator
- // of the AsyncButton which based on the externally supplied viewState.
- model.presentingRemovalAlert = true
- }) {
- Text("DELETE_ACCOUNT", bundle: .module)
- }
- } else {
- AsyncButton(role: .destructive, state: $destructiveViewState, action: {
- model.presentingLogoutAlert = true
- }) {
- Text("UP_LOGOUT", bundle: .module)
+
+ additionalSections
+
+ Section {
+ HStack {
+ if editMode?.wrappedValue.isEditing == true {
+ AsyncButton(role: .destructive, state: $destructiveViewState, action: {
+ // While the action closure itself is not async, we rely on ability to render loading indicator
+ // of the AsyncButton which based on the externally supplied viewState.
+ model.presentingRemovalAlert = true
+ }) {
+ Text("DELETE_ACCOUNT", bundle: .module)
+ }
+ } else {
+ AsyncButton(role: .destructive, state: $destructiveViewState, action: {
+ model.presentingLogoutAlert = true
+ }) {
+ Text("UP_LOGOUT", bundle: .module)
+ }
}
}
+ .frame(maxWidth: .infinity, alignment: .center)
}
- .frame(maxWidth: .infinity, alignment: .center)
}
-
+
@ViewBuilder private var sectionsView: some View {
ForEach(model.editableAccountKeys(details: accountDetails).elements, id: \.key) { category, accountKeys in
if !sectionIsEmpty(accountKeys) {
@@ -167,7 +172,7 @@ struct AccountOverviewSections: View {
let forEachWrappers = accountKeys.map { key in
ForEachAccountKeyWrapper(key)
}
-
+
ForEach(forEachWrappers, id: \.id) { wrapper in
AccountKeyEditRow(details: accountDetails, for: wrapper.accountKey, model: model)
}
@@ -182,43 +187,44 @@ struct AccountOverviewSections: View {
}
}
}
-
-
+
init(
account: Account,
details accountDetails: AccountDetails,
- isEditing: Binding
+ isEditing: Binding,
+ @ViewBuilder additionalSections: (() -> AdditionalSections) = { EmptyView() }
) {
self.accountDetails = accountDetails
self._model = StateObject(wrappedValue: AccountOverviewFormViewModel(account: account))
self._isEditing = isEditing
+ self.additionalSections = additionalSections()
}
-
-
+
+
private func editButtonAction() async throws {
if editMode?.wrappedValue.isEditing == false {
editMode?.wrappedValue = .active
return
}
-
+
guard !model.modifiedDetailsBuilder.isEmpty else {
logger.debug("Not saving anything, as there were no changes!")
model.discardChangesAction(editMode: editMode)
return
}
-
+
guard model.validationEngines.validateSubviews(focusState: $focusedDataEntry) else {
logger.debug("Some input validation failed. Staying in edit mode!")
return
}
-
+
focusedDataEntry = nil
-
+
logger.debug("Exiting edit mode and saving \(model.modifiedDetailsBuilder.count) changes to AccountService!")
-
+
try await model.updateAccountDetails(details: accountDetails, editMode: editMode)
}
-
+
/// Computes if a given `Section` is empty. This is the case if we are **not** currently editing
/// and the accountDetails don't have values stored for any of the provided ``AccountKey``.
private func sectionIsEmpty(_ accountKeys: [any AccountKey.Type]) -> Bool {
@@ -226,7 +232,7 @@ struct AccountOverviewSections: View {
// there is always UI presented in EDIT mode
return false
}
-
+
// we don't have to check for `addedAccountKeys` as these are only relevant in edit mode
return accountKeys.allSatisfy { element in
!accountDetails.contains(element)
@@ -241,10 +247,23 @@ struct AccountOverviewSections_Previews: PreviewProvider {
.set(\.userId, value: "andi.bauer@tum.de")
.set(\.name, value: PersonNameComponents(givenName: "Andreas", familyName: "Bauer"))
.set(\.genderIdentity, value: .male)
-
+
static var previews: some View {
NavigationStack {
- AccountOverview()
+ AccountOverview {
+ Section(header: Text("App")) {
+ NavigationLink {
+ Text("")
+ } label: {
+ Text("General Settings")
+ }
+ NavigationLink {
+ Text("")
+ } label: {
+ Text("License Information")
+ }
+ }
+ }
}
.environmentObject(Account(building: details, active: MockUserIdPasswordAccountService()))
}
diff --git a/Sources/SpeziAccount/Views/AccountOverview/PasswordChangeSheet.swift b/Sources/SpeziAccount/Views/AccountOverview/PasswordChangeSheet.swift
index 8d9ab366..7ea903f6 100644
--- a/Sources/SpeziAccount/Views/AccountOverview/PasswordChangeSheet.swift
+++ b/Sources/SpeziAccount/Views/AccountOverview/PasswordChangeSheet.swift
@@ -84,7 +84,6 @@ struct PasswordChangeSheet: View {
Divider()
.gridCellUnsizedAxes(.horizontal)
-
PasswordKey.DataEntry($repeatPassword)
.environment(\.passwordFieldType, .repeat)
.focused($focusedDataEntry, equals: "$-newPassword")
diff --git a/Sources/SpeziAccount/Views/AccountSummaryBox.swift b/Sources/SpeziAccount/Views/AccountSummaryBox.swift
index 464454f2..fd9920cd 100644
--- a/Sources/SpeziAccount/Views/AccountSummaryBox.swift
+++ b/Sources/SpeziAccount/Views/AccountSummaryBox.swift
@@ -58,6 +58,7 @@ public struct AccountSummaryBox: View {
}
}
+
#if DEBUG
struct AccountSummary_Previews: PreviewProvider {
static let emailDetails = AccountDetails.Builder()
diff --git a/Tests/UITests/TestApp/AccountTests/AccountTestsView.swift b/Tests/UITests/TestApp/AccountTests/AccountTestsView.swift
index 2d05cbaf..7ea8b1fd 100644
--- a/Tests/UITests/TestApp/AccountTests/AccountTestsView.swift
+++ b/Tests/UITests/TestApp/AccountTests/AccountTestsView.swift
@@ -14,14 +14,14 @@ import SwiftUI
struct AccountTestsView: View {
@Environment(\.features) var features
-
+
@EnvironmentObject var account: Account
@EnvironmentObject var standard: TestStandard
-
+
@State var showSetup = false
@State var showOverview = false
@State var isEditing = false
-
+
var body: some View {
NavigationStack {
@@ -34,33 +34,40 @@ struct AccountTestsView: View {
showOverview = true
}
}
- .navigationTitle("Spezi Account")
- .sheet(isPresented: $showSetup) {
- NavigationStack {
- AccountSetup {
- finishButton
- }
- .toolbar {
- toolbar(closing: $showSetup)
- }
+ .navigationTitle("Spezi Account")
+ .sheet(isPresented: $showSetup) {
+ NavigationStack {
+ AccountSetup {
+ finishButton
}
- }
- .sheet(isPresented: $showOverview) {
- NavigationStack {
- AccountOverview(isEditing: $isEditing)
- .toolbar {
- toolbar(closing: $showOverview)
- }
+ .toolbar {
+ toolbar(closing: $showSetup)
}
}
- .onChange(of: account.signedIn) { newValue in
- if newValue {
- showSetup = false
+ }
+ .sheet(isPresented: $showOverview) {
+ NavigationStack {
+ AccountOverview(isEditing: $isEditing) {
+ NavigationLink {
+ Text("")
+ .navigationTitle(Text("Package Dependencies"))
+ } label: {
+ Text("License Information")
+ }
}
}
+ .toolbar {
+ toolbar(closing: $showOverview)
+ }
+ }
+ }
+ .onChange(of: account.signedIn) { newValue in
+ if newValue {
+ showSetup = false
+ }
}
}
-
+
@ViewBuilder var header: some View {
if let details = account.details {
Section("Account Details") {
@@ -82,7 +89,7 @@ struct AccountTestsView: View {
Text("Finish")
.frame(maxWidth: .infinity, minHeight: 38)
})
- .buttonStyle(.borderedProminent)
+ .buttonStyle(.borderedProminent)
}
@@ -105,14 +112,14 @@ struct AccountTestsView_Previews: PreviewProvider {
.set(\.userId, value: "andi.bauer@tum.de")
.set(\.name, value: PersonNameComponents(givenName: "Andreas", familyName: "Bauer"))
.set(\.genderIdentity, value: .male)
-
+
static var previews: some View {
AccountTestsView()
.environmentObject(Account(TestAccountService(.emailAddress)))
-
+
AccountTestsView()
.environmentObject(Account(building: details, active: TestAccountService(.emailAddress)))
-
+
AccountTestsView()
.environmentObject(Account())
}
diff --git a/Tests/UITests/TestAppUITests/AccountOverviewTests.swift b/Tests/UITests/TestAppUITests/AccountOverviewTests.swift
index 7be27dde..d29f131c 100644
--- a/Tests/UITests/TestAppUITests/AccountOverviewTests.swift
+++ b/Tests/UITests/TestAppUITests/AccountOverviewTests.swift
@@ -30,6 +30,8 @@ final class AccountOverviewTests: XCTestCase {
overview.verifyExistence(text: "Gender Identity, Male")
overview.verifyExistence(text: "Date of Birth, Mar 9, 1824")
+
+ overview.verifyExistence(text: "License Information")
XCTAssertTrue(overview.buttons["Logout"].waitForExistence(timeout: 0.5))
}
@@ -244,4 +246,13 @@ final class AccountOverviewTests: XCTestCase {
XCTAssertFalse(overview.secureTextFields["enter password"].waitForExistence(timeout: 2.0))
}
+
+ func testLicenseOverview() throws {
+ let app = TestApp.launch(defaultCredentials: true)
+ let overview = app.openAccountOverview()
+
+ overview.tap(button: "License Information")
+ sleep(2)
+ XCTAssertTrue(overview.navigationBars.staticTexts["Package Dependencies"].waitForExistence(timeout: 6.0))
+ }
}