Fix various flakey tests.

This commit is contained in:
Doug 2024-09-19 11:03:26 +01:00 committed by Doug
parent d2d3b28dc7
commit 4996137006
16 changed files with 87 additions and 66 deletions

View File

@ -31,23 +31,54 @@ struct SwipeRightAction<Label: View>: ViewModifier {
content content
.offset(x: xOffset, y: 0.0) .offset(x: xOffset, y: 0.0)
.animation(.interactiveSpring().speed(0.5), value: xOffset) .animation(.interactiveSpring().speed(0.5), value: xOffset)
.gesture(DragGesture() .simultaneousGesture(gesture)
.onChange(of: dragGestureActive) { value in
if value == true {
if shouldStartAction() {
feedbackGenerator.prepare()
canStartAction = true
}
}
}
.overlay(alignment: .leading) {
// We want the action icon to follow the view translation and gradually fade in
label()
.multilineTextAlignment(.center)
.frame(maxWidth: actionThreshold)
.opacity(xOffset / 50)
.animation(.interactiveSpring().speed(0.5), value: xOffset)
.offset(x: -actionThreshold + min(xOffset, actionThreshold), y: 0.0)
}
}
private var gesture: some Gesture {
DragGesture()
.updating($dragGestureActive) { _, state, _ in .updating($dragGestureActive) { _, state, _ in
// Available actions should be computed on the fly so we use a gesture state change // Available actions should be computed on the fly so we use a gesture state change
// to ask whether the move should be started or not. // to ask whether the move should be started or not.
state = true state = true
} }
.onChanged { value in .onChanged { value in
guard canStartAction else { guard canStartAction, value.translation.width > value.translation.height else {
return return
} }
// We want to add a spring like behavior to the drag in which the view // Due to https://forums.developer.apple.com/forums/thread/760035 we had to make
// the drag a simultaneous gesture otherwise it was impossible to scroll the timeline.
// Therefore we need to prevent the animation to run if the user is to scrolling vertically.
// It would be nice if we could somehow abort the gesture in this case.
let width: CGFloat = if value.translation.width > abs(value.translation.height) {
value.translation.width
} else {
0.0
}
// We want to add a spring like behaviour to the drag in which the view
// moves slower the more it's dragged. We use a circular easing function // moves slower the more it's dragged. We use a circular easing function
// to generate those values up to the `swipeThreshold` // to generate those values up to the `swipeThreshold`
// The final translation will be between 0 and `swipeThreshold` with the action being enabled from // The final translation will be between 0 and `swipeThreshold` with the action being enabled from
// `actionThreshold` onwards // `actionThreshold` onwards
let screenWidthNormalisedTranslation = max(0.0, min(value.translation.width, swipeThreshold)) / swipeThreshold let screenWidthNormalisedTranslation = max(0.0, min(width, swipeThreshold)) / swipeThreshold
let easedTranslation = circularEaseOut(screenWidthNormalisedTranslation) let easedTranslation = circularEaseOut(screenWidthNormalisedTranslation)
xOffset = easedTranslation * xOffsetThreshold xOffset = easedTranslation * xOffsetThreshold
@ -66,23 +97,6 @@ struct SwipeRightAction<Label: View>: ViewModifier {
} }
xOffset = 0.0 xOffset = 0.0
})
.onChange(of: dragGestureActive) { value in
if value == true {
if shouldStartAction() {
feedbackGenerator.prepare()
canStartAction = true
}
}
}
.overlay(alignment: .leading) {
// We want the action icon to follow the view translation and gradually fade in
label()
.multilineTextAlignment(.center)
.frame(maxWidth: actionThreshold)
.opacity(xOffset / 50)
.animation(.interactiveSpring().speed(0.5), value: xOffset)
.offset(x: -actionThreshold + min(xOffset, actionThreshold), y: 0.0)
} }
} }

View File

@ -379,6 +379,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
return return
} }
await self?.updatePinnedEventIDs() await self?.updatePinnedEventIDs()
await self?.updatePermissions()
} }
} }
.store(in: &cancellables) .store(in: &cancellables)

View File

@ -380,10 +380,13 @@ class TimelineViewModelTests: XCTestCase {
func testPinnedEvents() async throws { func testPinnedEvents() async throws {
ServiceLocator.shared.settings.pinningEnabled = true ServiceLocator.shared.settings.pinningEnabled = true
// Note: We need to start the test with a non-default value so we know the view model has finished the Task.
let roomProxyMock = JoinedRoomProxyMock(.init(name: "", let roomProxyMock = JoinedRoomProxyMock(.init(name: "",
pinnedEventIDs: .init(["test1"]))) pinnedEventIDs: .init(["test1"])))
let actionsSubject = PassthroughSubject<JoinedRoomProxyAction, Never>() let actionsSubject = PassthroughSubject<JoinedRoomProxyAction, Never>()
roomProxyMock.underlyingActionsPublisher = actionsSubject.eraseToAnyPublisher() roomProxyMock.underlyingActionsPublisher = actionsSubject.eraseToAnyPublisher()
let viewModel = TimelineViewModel(roomProxy: roomProxyMock, let viewModel = TimelineViewModel(roomProxy: roomProxyMock,
timelineController: MockRoomTimelineController(), timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
@ -409,9 +412,12 @@ class TimelineViewModelTests: XCTestCase {
func testCanUserPinEvents() async throws { func testCanUserPinEvents() async throws {
ServiceLocator.shared.settings.pinningEnabled = true ServiceLocator.shared.settings.pinningEnabled = true
let roomProxyMock = JoinedRoomProxyMock(.init(name: "", canUserPin: false))
// Note: We need to start the test with the non-default value so we know the view model has finished the Task.
let roomProxyMock = JoinedRoomProxyMock(.init(name: "", canUserPin: true))
let actionsSubject = PassthroughSubject<JoinedRoomProxyAction, Never>() let actionsSubject = PassthroughSubject<JoinedRoomProxyAction, Never>()
roomProxyMock.underlyingActionsPublisher = actionsSubject.eraseToAnyPublisher() roomProxyMock.underlyingActionsPublisher = actionsSubject.eraseToAnyPublisher()
let viewModel = TimelineViewModel(roomProxy: roomProxyMock, let viewModel = TimelineViewModel(roomProxy: roomProxyMock,
timelineController: MockRoomTimelineController(), timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(), mediaProvider: MockMediaProvider(),
@ -423,13 +429,13 @@ class TimelineViewModelTests: XCTestCase {
analyticsService: ServiceLocator.shared.analytics) analyticsService: ServiceLocator.shared.analytics)
var deferred = deferFulfillment(viewModel.context.$viewState) { value in var deferred = deferFulfillment(viewModel.context.$viewState) { value in
!value.canCurrentUserPin value.canCurrentUserPin
} }
try await deferred.fulfill() try await deferred.fulfill()
roomProxyMock.canUserPinOrUnpinUserIDReturnValue = .success(true) roomProxyMock.canUserPinOrUnpinUserIDReturnValue = .success(false)
deferred = deferFulfillment(viewModel.context.$viewState) { value in deferred = deferFulfillment(viewModel.context.$viewState) { value in
value.canCurrentUserPin !value.canCurrentUserPin
} }
actionsSubject.send(.roomInfoUpdate) actionsSubject.send(.roomInfoUpdate)
try await deferred.fulfill() try await deferred.fulfill()