Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Map Operation States to View States #24

Merged
merged 9 commits into from
Nov 29, 2023
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add OperationStateAlert and tests
philippzagar committed Nov 29, 2023
commit 23c6022816d6b0377c71268f79151394e26479c5
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import SwiftUI


private struct OperationStateAlert<T: OperationState>: ViewModifier {
private let operationState: T
@State private var viewState: ViewState

init(operationState: T) {
self.operationState = operationState
self._viewState = State(wrappedValue: operationState.representation)
}


func body(content: Content) -> some View {
content
.map(state: operationState, to: $viewState)
.viewStateAlert(state: $viewState)
}
}


extension View {
/// Automatically displays an alert using the localized error descriptions based on an ``ViewState`` derived from a ``OperationState``.
/// - Parameter state: The ``OperationState`` from which the ``ViewState`` is derived.
public func viewStateAlert<T: OperationState>(state: T) -> some View {
philippzagar marked this conversation as resolved.
Show resolved Hide resolved
self
.modifier(OperationStateAlert(operationState: state))
}
}
70 changes: 70 additions & 0 deletions Tests/UITests/TestApp/ViewsTests/OperationStateTestView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//
// This source file is part of the Stanford Spezi open-source project
//
// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import SpeziViews
import SwiftUI


struct OperationStateTestView: View {
enum OperationStateTest: OperationState {
case ready
case someOperationStep
case error(LocalizedError)


var representation: ViewState {
switch self {
case .ready:
.idle
case .someOperationStep:
.processing
case .error(let localizedError):
.error(localizedError)
}
}
}

struct TestError: LocalizedError {
var errorDescription: String?
var failureReason: String?
var helpAnchor: String?
var recoverySuggestion: String?
}

let testError = TestError(
errorDescription: nil,
failureReason: "Failure Reason",
helpAnchor: "Help Anchor",
recoverySuggestion: "Recovery Suggestion"
)

@State var operationState: OperationStateTest = .ready

var body: some View {
VStack {
Text("Operation State: \(String(describing: operationState))")
.accessibilityIdentifier("operationState")
}
.task {
operationState = .someOperationStep
try? await Task.sleep(for: .seconds(10))
operationState = .error(
AnyLocalizedError(
error: testError,
defaultErrorDescription: "Error Description"
)
)
}
.viewStateAlert(state: operationState)
}
}


#Preview {
OperationStateTestView()
}
14 changes: 11 additions & 3 deletions Tests/UITests/TestApp/ViewsTests/SpeziViewsTests.swift
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ enum SpeziViewsTests: String, TestAppTests {
case lazyText = "Lazy Text"
case markdownView = "Markdown View"
case viewState = "View State"
case operationState = "Operation State"
case viewStateMapper = "View State Mapper"
case defaultErrorOnly = "Default Error Only"
case defaultErrorDescription = "Default Error Description"
@@ -78,17 +79,22 @@ enum SpeziViewsTests: String, TestAppTests {
private var viewState: some View {
ViewStateTestView()
}

@ViewBuilder
private var defaultErrorOnly: some View {
ViewStateTestView(testError: .init(errorDescription: "Some error occurred!"))
private var operationState: some View {
OperationStateTestView()
}

@ViewBuilder
private var viewStateMapper: some View {
ViewStateMapperTestView()
}

@ViewBuilder
private var defaultErrorOnly: some View {
ViewStateTestView(testError: .init(errorDescription: "Some error occurred!"))
}

@ViewBuilder
private var defaultErrorDescription: some View {
DefaultErrorDescriptionTestView()
@@ -114,6 +120,8 @@ enum SpeziViewsTests: String, TestAppTests {
markdownView
case .viewState:
viewState
case .operationState:
operationState
case .viewStateMapper:
viewStateMapper
case .defaultErrorOnly:
20 changes: 20 additions & 0 deletions Tests/UITests/TestAppUITests/SpeziViews/ModelTests.swift
philippzagar marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -40,6 +40,26 @@ final class ModelTests: XCTestCase {
XCTAssert(app.staticTexts["View State: idle"].waitForExistence(timeout: 2))
}

func testOperationState() throws {
let app = XCUIApplication()

XCTAssert(app.collectionViews.buttons["Operation State"].waitForExistence(timeout: 2))
app.collectionViews.buttons["Operation State"].tap()

XCTAssert(app.staticTexts["Operation State: someOperationStep"].waitForExistence(timeout: 2))

sleep(12)

let alert = app.alerts.firstMatch.scrollViews.otherElements
XCTAssert(alert.staticTexts["Error Description"].exists)
XCTAssert(alert.staticTexts["Failure Reason\n\nHelp Anchor\n\nRecovery Suggestion"].exists)
alert.buttons["OK"].tap()

sleep(2)

XCTAssert(app.staticTexts["operationState"].label.contains("Operation State: error"))
}

func testViewStateMapper() throws {
let app = XCUIApplication()

4 changes: 4 additions & 0 deletions Tests/UITests/UITests.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@
2FB099B82A8AD25300B20952 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 2FB099B72A8AD25100B20952 /* Localizable.xcstrings */; };
9731B58F2B167053007676C0 /* ViewStateMapperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9731B58E2B167053007676C0 /* ViewStateMapperView.swift */; };
977CF55C2AD2B92C006D9B54 /* XCTestApp in Frameworks */ = {isa = PBXBuildFile; productRef = 977CF55B2AD2B92C006D9B54 /* XCTestApp */; };
97EE16AC2B16D5AB004D25A3 /* OperationStateTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97EE16AB2B16D5AB004D25A3 /* OperationStateTestView.swift */; };
A95B6E652AF4298500919504 /* SpeziPersonalInfo in Frameworks */ = {isa = PBXBuildFile; productRef = A95B6E642AF4298500919504 /* SpeziPersonalInfo */; };
A963ACAC2AF4683A00D745F2 /* SpeziValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A963ACAB2AF4683A00D745F2 /* SpeziValidationTests.swift */; };
A963ACB02AF4692500D745F2 /* SpeziValidation in Frameworks */ = {isa = PBXBuildFile; productRef = A963ACAF2AF4692500D745F2 /* SpeziValidation */; };
@@ -63,6 +64,7 @@
2FB0758A299DDB9000C0B37F /* TestApp.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestApp.xctestplan; sourceTree = "<group>"; };
2FB099B72A8AD25100B20952 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
9731B58E2B167053007676C0 /* ViewStateMapperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewStateMapperView.swift; sourceTree = "<group>"; };
97EE16AB2B16D5AB004D25A3 /* OperationStateTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationStateTestView.swift; sourceTree = "<group>"; };
A963ACAB2AF4683A00D745F2 /* SpeziValidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeziValidationTests.swift; sourceTree = "<group>"; };
A963ACB12AF4709400D745F2 /* XCUIApplication+Targets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCUIApplication+Targets.swift"; sourceTree = "<group>"; };
A97880962A4C4E6500150B2F /* ModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelTests.swift; sourceTree = "<group>"; };
@@ -159,6 +161,7 @@
2F2D338629DE52EA00081B1D /* SpeziViewsTests.swift */,
2FA9485E29DE90720081C086 /* ViewStateTestView.swift */,
9731B58E2B167053007676C0 /* ViewStateMapperView.swift */,
97EE16AB2B16D5AB004D25A3 /* OperationStateTestView.swift */,
2FA9485F29DE90720081C086 /* MarkdownViewTestView.swift */,
2FA9486029DE90720081C086 /* CanvasTestView.swift */,
2FA9486329DE90720081C086 /* GeometryReaderTestView.swift */,
@@ -330,6 +333,7 @@
A9FBAE952AF445B6001E4AF1 /* SpeziViewsTargetsTests.swift in Sources */,
2FA9486929DE90720081C086 /* NameFieldsTestView.swift in Sources */,
A99A65122AF57CA200E63582 /* FocusedValidationTests.swift in Sources */,
97EE16AC2B16D5AB004D25A3 /* OperationStateTestView.swift in Sources */,
9731B58F2B167053007676C0 /* ViewStateMapperView.swift in Sources */,
A998A94F2A609A9E0030624D /* AsyncButtonTestView.swift in Sources */,
2FA9486A29DE90720081C086 /* GeometryReaderTestView.swift in Sources */,