Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
61 changes: 22 additions & 39 deletions ElementX.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,8 @@ struct PreviewsWrapperView: View {
// this is a temporary solution
.timeout(.seconds(1), scheduler: DispatchQueue.main)
.values.first { $0 == true }
case .stream(let stream):
_ = await stream.first { $0 == true }
case .sequence(let sequence):
_ = await sequence.first { $0 == true }
case .none:
break
}
Expand Down
15 changes: 0 additions & 15 deletions ElementX/Sources/Other/Extensions/AsyncSequence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,4 @@ extension AsyncSequence {
func first() async rethrows -> Self.Element? {
try await first { _ in true }
}

/// Type-erases the sequence into a newly constructed asynchronous stream. This is useful until
/// we drop support for iOS 17, at which point we can replace this with `any AsyncSequence`.
@available(iOS, deprecated: 18.0, message: "Use `any AsyncSequence` instead.")
func eraseToStream() -> AsyncStream<Element> {
var asyncIterator = makeAsyncIterator()
return AsyncStream<Element> {
do {
return try await asyncIterator.next()
} catch {
MXLog.warning("Stopping stream: \(error)")
return nil
}
}
}
}
2 changes: 1 addition & 1 deletion ElementX/Sources/Other/Extensions/Observable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
//

import Foundation
import Mutex
import Synchronization

extension Observable {
/// Creates an async stream for the specified property on this object. We probably won't need this once SE-0475 is available:
Expand Down
8 changes: 4 additions & 4 deletions ElementX/Sources/Other/Extensions/Snapshotting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ struct SnapshotFulfillmentPreferenceKey: PreferenceKey {

enum Source {
case publisher(AnyPublisher<Bool, Never>)
case stream(AsyncStream<Bool>)
case sequence(any AsyncSequence<Bool, Never>)
}

struct Wrapper: Equatable {
Expand Down Expand Up @@ -69,14 +69,14 @@ extension SwiftUI.View {
/// These preferences can then be retrieved and used elsewhere in your view hierarchy.
///
/// - Parameters:
/// - expect: A stream that indicates when the preview is ready for snapshotting.
/// - expect: An async sequence that indicates when the preview is ready for snapshotting.
/// - precision: The percentage of pixels that must match.
/// - perceptualPrecision: The percentage a pixel must match the source pixel to be considered a match. 98-99% mimics the precision of the human eye.
func snapshotPreferences(expect fulfillmentStream: AsyncStream<Bool>? = nil,
func snapshotPreferences(expect fulfillmentSequence: (any AsyncSequence<Bool, Never>)? = nil,
precision: Float = 1.0,
perceptualPrecision: Float = 0.98) -> some SwiftUI.View {
preference(key: SnapshotPrecisionPreferenceKey.self, value: precision)
.preference(key: SnapshotPerceptualPrecisionPreferenceKey.self, value: perceptualPrecision)
.preference(key: SnapshotFulfillmentPreferenceKey.self, value: fulfillmentStream.map { SnapshotFulfillmentPreferenceKey.Wrapper(source: .stream($0)) })
.preference(key: SnapshotFulfillmentPreferenceKey.self, value: fulfillmentSequence.map { SnapshotFulfillmentPreferenceKey.Wrapper(source: .sequence($0)) })
}
}
38 changes: 19 additions & 19 deletions ElementX/Sources/Other/Extensions/XCTestCase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,23 +49,23 @@ extension XCTestCase {
}
}

/// XCTest utility that assists in observing an async stream, deferring the fulfilment and results until some condition has been met.
/// XCTest utility that assists in observing an async sequence, deferring the fulfilment and results until some condition has been met.
/// - Parameters:
/// - asyncStream: The stream to wait on.
/// - asyncSequence: The sequence to wait on.
/// - timeout: A timeout after which we give up.
/// - message: An optional custom expectation message
/// - until: callback that evaluates outputs until some condition is reached
/// - Returns: The deferred fulfilment to be executed after some actions and that returns the result of the stream.
func deferFulfillment<Value>(_ asyncStream: AsyncStream<Value>,
/// - Returns: The deferred fulfilment to be executed after some actions and that returns the result of the sequence.
func deferFulfillment<Value>(_ asyncSequence: any AsyncSequence<Value, Never>,
timeout: TimeInterval = 10,
message: String? = nil,
until condition: @escaping (Value) -> Bool) -> DeferredFulfillment<Value> {
var result: Result<Value, Error>?
let expectation = expectation(description: message ?? "Awaiting stream")
let expectation = expectation(description: message ?? "Awaiting sequence")
var hasFulfilled = false

let task = Task {
for await value in asyncStream {
for await value in asyncSequence {
if condition(value), !hasFulfilled {
result = .success(value)
expectation.fulfill()
Expand All @@ -77,7 +77,7 @@ extension XCTestCase {
return DeferredFulfillment<Value> {
await self.fulfillment(of: [expectation], timeout: timeout)
task.cancel()
let unwrappedResult = try XCTUnwrap(result, "Awaited stream did not produce any output")
let unwrappedResult = try XCTUnwrap(result, "Awaited sequence did not produce any output")
return try unwrappedResult.get()
}
}
Expand Down Expand Up @@ -108,19 +108,19 @@ extension XCTestCase {
return deferred
}

/// XCTest utility that assists in subscribing to an async stream and deferring the fulfilment and results until some other actions have been performed.
/// XCTest utility that assists in subscribing to an async sequence and deferring the fulfilment and results until some other actions have been performed.
/// - Parameters:
/// - asyncStream: The stream to wait on.
/// - transitionValues: the values through which the stream needs to transition through
/// - asyncSequence: The sequence to wait on.
/// - transitionValues: the values through which the sequence needs to transition through
/// - timeout: A timeout after which we give up.
/// - message: An optional custom expectation message
/// - Returns: The deferred fulfilment to be executed after some actions and that returns the result of the stream.
func deferFulfillment<Value: Equatable>(_ asyncStream: AsyncStream<Value>,
/// - Returns: The deferred fulfilment to be executed after some actions and that returns the result of the sequence.
func deferFulfillment<Value: Equatable>(_ asyncSequence: any AsyncSequence<Value, Never>,
transitionValues: [Value],
timeout: TimeInterval = 10,
message: String? = nil) -> DeferredFulfillment<Value> {
var expectedOrder = transitionValues
let deferred = deferFulfillment(asyncStream, timeout: timeout, message: message) { value in
let deferred = deferFulfillment(asyncSequence, timeout: timeout, message: message) { value in
if let index = expectedOrder.firstIndex(where: { $0 == value }), index == 0 {
expectedOrder.remove(at: index)
}
Expand Down Expand Up @@ -159,23 +159,23 @@ extension XCTestCase {
}
}

/// XCTest utility that assists in subscribing to an async stream and deferring the failure for a particular value until some other actions have been performed.
/// XCTest utility that assists in subscribing to an async sequence and deferring the failure for a particular value until some other actions have been performed.
/// - Parameters:
/// - asyncStream: The stream to wait on.
/// - asyncSequence: The sequence to wait on.
/// - timeout: A timeout after which we give up.
/// - message: An optional custom expectation message
/// - until: callback that evaluates outputs until some condition is reached
/// - Returns: The deferred fulfilment to be executed after some actions. The stream's result is not returned from this fulfilment.
func deferFailure<Value>(_ asyncStream: AsyncStream<Value>,
/// - Returns: The deferred fulfilment to be executed after some actions. The sequence's result is not returned from this fulfilment.
func deferFailure<Value>(_ asyncSequence: any AsyncSequence<Value, Never>,
timeout: TimeInterval,
message: String? = nil,
until condition: @escaping (Value) -> Bool) -> DeferredFulfillment<Void> {
let expectation = expectation(description: message ?? "Awaiting stream")
let expectation = expectation(description: message ?? "Awaiting sequence")
expectation.isInverted = true
var hasFulfilled = false

let task = Task {
for await value in asyncStream {
for await value in asyncSequence {
if condition(value), !hasFulfilled {
expectation.fulfill()
hasFulfilled = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,13 @@ struct LoginScreen_Previews: PreviewProvider, TestablePreview {
NavigationStack {
LoginScreen(context: viewModel.context)
}
.snapshotPreferences(expect: viewModel.context.observe(\.viewState.homeserver.loginMode).map { $0 == .password }.eraseToStream())
.snapshotPreferences(expect: viewModel.context.observe(\.viewState.homeserver.loginMode).map { $0 == .password })
.previewDisplayName("Initial State")

NavigationStack {
LoginScreen(context: credentialsViewModel.context)
}
.snapshotPreferences(expect: credentialsViewModel.context.observe(\.viewState.homeserver.loginMode).map { $0 == .password }.eraseToStream())
.snapshotPreferences(expect: credentialsViewModel.context.observe(\.viewState.homeserver.loginMode).map { $0 == .password })
.previewDisplayName("Credentials Entered")

NavigationStack {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ struct EmojiPickerScreen_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
EmojiPickerScreen(context: viewModel.context)
.previewDisplayName("Screen")
.snapshotPreferences(expect: viewModel.context.observe(\.viewState.categories).map { !$0.isEmpty }.eraseToStream())
.snapshotPreferences(expect: viewModel.context.observe(\.viewState.categories).map { !$0.isEmpty })
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ struct TimelineMediaPreviewDetailsView_Previews: PreviewProvider, TestablePrevie
if case let .media(mediaItem) = viewModel.state.currentItem {
TimelineMediaPreviewDetailsView(item: mediaItem, context: viewModel.context, sheetHeight: $sheetHeight)
.previewDisplayName("Image")
.snapshotPreferences(expect: mediaItem.observe(\.fileHandle).map { $0 != nil }.eraseToStream())
.snapshotPreferences(expect: mediaItem.observe(\.fileHandle).map { $0 != nil })
}

if case let .media(mediaItem) = loadingViewModel.state.currentItem {
Expand All @@ -196,13 +196,13 @@ struct TimelineMediaPreviewDetailsView_Previews: PreviewProvider, TestablePrevie
if case let .media(mediaItem) = unknownTypeViewModel.state.currentItem {
TimelineMediaPreviewDetailsView(item: mediaItem, context: unknownTypeViewModel.context, sheetHeight: $sheetHeight)
.previewDisplayName("Unknown type")
.snapshotPreferences(expect: mediaItem.observe(\.fileHandle).map { $0 != nil }.eraseToStream())
.snapshotPreferences(expect: mediaItem.observe(\.fileHandle).map { $0 != nil })
}

if case let .media(mediaItem) = presentedOnRoomViewModel.state.currentItem {
TimelineMediaPreviewDetailsView(item: mediaItem, context: presentedOnRoomViewModel.context, sheetHeight: $sheetHeight)
.previewDisplayName("Incoming on Room")
.snapshotPreferences(expect: mediaItem.observe(\.fileHandle).map { $0 != nil }.eraseToStream())
.snapshotPreferences(expect: mediaItem.observe(\.fileHandle).map { $0 != nil })
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ struct IdentityConfirmationScreen_Previews: PreviewProvider, TestablePreview {
.previewDisplayName("Actions")
.snapshotPreferences(expect: viewModel.context.observe(\.viewState.availableActions).map { actions in
actions?.contains([.interactiveVerification, .recovery]) == true
}.eraseToStream())
})

NavigationStack {
IdentityConfirmationScreen(context: loadingViewModel.context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,23 +339,23 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {

static var previews: some View {
RoomDetailsScreen(context: genericRoomViewModel.context)
.snapshotPreferences(expect: genericRoomViewModel.context.observe(\.viewState.permalink).map { $0 != nil }.eraseToStream())
.snapshotPreferences(expect: genericRoomViewModel.context.observe(\.viewState.permalink).map { $0 != nil })
.previewDisplayName("Generic Room")

RoomDetailsScreen(context: simpleRoomViewModel.context)
.snapshotPreferences(expect: simpleRoomViewModel.context.observe(\.viewState.permalink).map { $0 != nil }.eraseToStream())
.snapshotPreferences(expect: simpleRoomViewModel.context.observe(\.viewState.permalink).map { $0 != nil })
.previewDisplayName("Simple Room")

RoomDetailsScreen(context: dmRoomViewModel.context)
.snapshotPreferences(expect: dmRoomViewModel.context.observe(\.viewState.accountOwner).map { $0 != nil }.eraseToStream())
.snapshotPreferences(expect: dmRoomViewModel.context.observe(\.viewState.accountOwner).map { $0 != nil })
.previewDisplayName("DM Room")

RoomDetailsScreen(context: dmRoomVerifiedViewModel.context)
.snapshotPreferences(expect: dmRoomVerifiedViewModel.context.observe(\.viewState.dmRecipientInfo?.verificationState).map { $0 == .verified }.eraseToStream())
.snapshotPreferences(expect: dmRoomVerifiedViewModel.context.observe(\.viewState.dmRecipientInfo?.verificationState).map { $0 == .verified })
.previewDisplayName("DM Room Verified")

RoomDetailsScreen(context: dmRoomVerificationViolationViewModel.context)
.snapshotPreferences(expect: dmRoomVerificationViolationViewModel.context.observe(\.viewState.accountOwner).map { $0 != nil }.eraseToStream())
.snapshotPreferences(expect: dmRoomVerificationViolationViewModel.context.observe(\.viewState.accountOwner).map { $0 != nil })
.previewDisplayName("DM Room Verification Violation")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,26 +127,26 @@ struct SecureBackupLogoutConfirmationScreen_Previews: PreviewProvider, TestableP
SecureBackupLogoutConfirmationScreen(context: waitingViewModel.context)
}
.previewDisplayName("Waiting")
.snapshotPreferences(expect: waitingViewModel.context.observe(\.viewState.mode).map { $0 == .waitingToStart(hasStalled: false) }.eraseToStream())
.snapshotPreferences(expect: waitingViewModel.context.observe(\.viewState.mode).map { $0 == .waitingToStart(hasStalled: false) })

NavigationStack {
SecureBackupLogoutConfirmationScreen(context: ongoingViewModel.context)
}
.previewDisplayName("Ongoing")
.snapshotPreferences(expect: ongoingViewModel.context.observe(\.viewState.mode).map { $0 == .backupOngoing(progress: 0.5) }.eraseToStream())
.snapshotPreferences(expect: ongoingViewModel.context.observe(\.viewState.mode).map { $0 == .backupOngoing(progress: 0.5) })

// Uses the same view model as Waiting but with a different expectation.
NavigationStack {
SecureBackupLogoutConfirmationScreen(context: waitingViewModel.context)
}
.previewDisplayName("Stalled")
.snapshotPreferences(expect: waitingViewModel.context.observe(\.viewState.mode).map { $0 == .waitingToStart(hasStalled: true) }.eraseToStream())
.snapshotPreferences(expect: waitingViewModel.context.observe(\.viewState.mode).map { $0 == .waitingToStart(hasStalled: true) })

NavigationStack {
SecureBackupLogoutConfirmationScreen(context: offlineViewModel.context)
}
.previewDisplayName("Offline")
.snapshotPreferences(expect: offlineViewModel.context.observe(\.viewState.mode).map { $0 == .offline }.eraseToStream())
.snapshotPreferences(expect: offlineViewModel.context.observe(\.viewState.mode).map { $0 == .offline })
}

static func makeViewModel(mode: SecureBackupLogoutConfirmationScreenViewMode) -> SecureBackupLogoutConfirmationScreenViewModel {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ struct SecureBackupRecoveryKeyScreen_Previews: PreviewProvider, TestablePreview
NavigationStack {
SecureBackupRecoveryKeyScreen(context: setupViewModel.context)
}
.snapshotPreferences(expect: setupViewModel.context.observe(\.viewState.recoveryKey).map { $0 != nil }.eraseToStream())
.snapshotPreferences(expect: setupViewModel.context.observe(\.viewState.recoveryKey).map { $0 != nil })
.previewDisplayName("Set up")

NavigationStack {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,25 +129,25 @@ struct SecureBackupScreen_Previews: PreviewProvider, TestablePreview {
NavigationStack {
SecureBackupScreen(context: bothSetupViewModel.context)
}
.snapshotPreferences(expect: bothSetupViewModel.context.observe(\.viewState.keyBackupState).map { $0 == .enabled }.eraseToStream())
.snapshotPreferences(expect: bothSetupViewModel.context.observe(\.viewState.keyBackupState).map { $0 == .enabled })
.previewDisplayName("Both setup")

NavigationStack {
SecureBackupScreen(context: onlyKeyBackupSetUpViewModel.context)
}
.snapshotPreferences(expect: onlyKeyBackupSetUpViewModel.context.observe(\.viewState.keyBackupState).map { $0 == .enabled }.eraseToStream())
.snapshotPreferences(expect: onlyKeyBackupSetUpViewModel.context.observe(\.viewState.keyBackupState).map { $0 == .enabled })
.previewDisplayName("Only key backup setup")

NavigationStack {
SecureBackupScreen(context: keyBackupDisabledViewModel.context)
}
.snapshotPreferences(expect: keyBackupDisabledViewModel.context.observe(\.viewState.keyBackupState).map { $0 == .unknown }.eraseToStream())
.snapshotPreferences(expect: keyBackupDisabledViewModel.context.observe(\.viewState.keyBackupState).map { $0 == .unknown })
.previewDisplayName("Key backup disabled")

NavigationStack {
SecureBackupScreen(context: recoveryIncompleteViewModel.context)
}
.snapshotPreferences(expect: recoveryIncompleteViewModel.context.observe(\.viewState.recoveryState).map { $0 == .incomplete }.eraseToStream())
.snapshotPreferences(expect: recoveryIncompleteViewModel.context.observe(\.viewState.recoveryState).map { $0 == .incomplete })
.previewDisplayName("Recovery incomplete")
}

Expand Down
Loading
Loading