Start fixing flakey tests ❄️ (#3329)

* Wait longer on authentication flow tests.

* Move default perceptualPrecision value into the snapshot preferences.

* Delay snapshots *before* setting up the test.

* Reset the simulators on GitHub before running?

* Remove a test that is now handled by Rust.

* Fix a test that was yielding.
This commit is contained in:
Doug
2024-09-26 16:09:01 +01:00
committed by GitHub
parent 23ab453a1e
commit 4d7d687954
44 changed files with 138 additions and 153 deletions
+4 -1
View File
@@ -29,7 +29,10 @@ jobs:
- name: Setup environment
run:
source ci_scripts/ci_common.sh && setup_github_actions_environment
- name: Reset simulators
run: SNAPSHOT_FORCE_DELETE=1 bundle exec fastlane snapshot reset_simulators
- name: Run tests
run: bundle exec fastlane integration_tests
env:
+3
View File
@@ -35,6 +35,9 @@ jobs:
- name: SwiftFormat
run:
swiftformat --lint .
- name: Reset simulators
run: SNAPSHOT_FORCE_DELETE=1 bundle exec fastlane snapshot reset_simulators
- name: Run tests
run: bundle exec fastlane unit_tests
+4 -1
View File
@@ -44,7 +44,10 @@ jobs:
- name: SwiftFormat
run: swiftformat --lint .
- name: Reset simulators
run: SNAPSHOT_FORCE_DELETE=1 bundle exec fastlane snapshot reset_simulators
- name: Run tests
run: bundle exec fastlane unit_tests skip_previews:true
@@ -33,7 +33,7 @@ public struct SnapshotPrecisionPreferenceKey: PreferenceKey {
}
public struct SnapshotPerceptualPrecisionPreferenceKey: PreferenceKey {
public static var defaultValue: Float = 1.0
public static var defaultValue: Float = 0.98
public static func reduce(value: inout Float, nextValue: () -> Float) {
value = nextValue()
@@ -50,7 +50,7 @@ public extension SwiftUI.View {
/// - 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.
@inlinable
func snapshotPreferences(delay: TimeInterval = .zero, precision: Float = 1.0, perceptualPrecision: Float = 1.0) -> some SwiftUI.View {
func snapshotPreferences(delay: TimeInterval = .zero, precision: Float = 1.0, perceptualPrecision: Float = 0.98) -> some SwiftUI.View {
preference(key: SnapshotDelayPreferenceKey.self, value: delay)
.preference(key: SnapshotPrecisionPreferenceKey.self, value: precision)
.preference(key: SnapshotPerceptualPrecisionPreferenceKey.self, value: perceptualPrecision)
+49 -40
View File
@@ -11,6 +11,7 @@ import XCTest
@testable import ElementX
@testable import SnapshotTesting
@MainActor
class PreviewTests: XCTestCase {
private let deviceConfig: ViewImageConfig = .iPhoneX
private let simulatorDevice: String? = "iPhone14,6" // iPhone SE 3rd Generation
@@ -50,6 +51,22 @@ class PreviewTests: XCTestCase {
// MARK: - Snapshots
func assertSnapshots(matching preview: _Preview, testName: String = #function) {
let preferences = SnapshotPreferences()
let preferenceReadingView = preview.content
.onPreferenceChange(SnapshotDelayPreferenceKey.self) { preferences.delay = $0 }
.onPreferenceChange(SnapshotPrecisionPreferenceKey.self) { preferences.precision = $0 }
.onPreferenceChange(SnapshotPerceptualPrecisionPreferenceKey.self) { preferences.perceptualPrecision = $0 }
// Render an image of the view in order to trigger the preference updates to occur.
let imageRenderer = ImageRenderer(content: preferenceReadingView)
_ = imageRenderer.uiImage
// Delay the test now - a delay after creating the `snapshotView` results in the underlying view not getting updated for snapshotting.
if preferences.delay != .zero {
wait(for: preferences.delay)
}
for deviceName in snapshotDevices {
guard var device = PreviewDevice(rawValue: deviceName).snapshotDevice() else {
fatalError("Unknown device name: \(deviceName)")
@@ -58,12 +75,13 @@ class PreviewTests: XCTestCase {
device.safeArea = .one
// Ignore specific device display scale
let traits = UITraitCollection(displayScale: 2.0)
if let failure = assertSnapshots(matching: AnyView(preview.content),
if let failure = assertSnapshots(matching: preview.content,
name: preview.displayName,
isScreen: preview.layout == .device,
device: device,
testName: testName + deviceName + "-" + localeCode,
traits: traits) {
traits: traits,
preferences: preferences) {
XCTFail(failure)
}
}
@@ -85,19 +103,12 @@ class PreviewTests: XCTestCase {
}
private func assertSnapshots(matching view: AnyView,
name: String?, isScreen: Bool,
name: String?,
isScreen: Bool,
device: ViewImageConfig,
testName: String = #function,
traits: UITraitCollection = .init()) -> String? {
var delay: TimeInterval = 0
var precision: Float = 1
var perceptualPrecision: Float = 1
let view = view
.onPreferenceChange(SnapshotDelayPreferenceKey.self) { delay = $0 }
.onPreferenceChange(SnapshotPrecisionPreferenceKey.self) { precision = $0 }
.onPreferenceChange(SnapshotPerceptualPrecisionPreferenceKey.self) { perceptualPrecision = $0 }
traits: UITraitCollection = .init(),
preferences: SnapshotPreferences) -> String? {
let matchingView = isScreen ? AnyView(view) : AnyView(view
.frame(width: device.size?.width)
.fixedSize(horizontal: false, vertical: true)
@@ -105,15 +116,27 @@ class PreviewTests: XCTestCase {
return withSnapshotTesting(record: recordMode) {
verifySnapshot(of: matchingView,
as: .prefireImage(precision: { precision },
perceptualPrecision: { perceptualPrecision },
duration: { delay },
as: .prefireImage(preferences: preferences,
layout: isScreen ? .device(config: device) : .sizeThatFits,
traits: traits),
named: name,
testName: testName)
}
}
private func wait(for duration: TimeInterval) {
let expectation = XCTestExpectation(description: "Wait")
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
expectation.fulfill()
}
_ = XCTWaiter.wait(for: [expectation], timeout: duration + 1)
}
}
private class SnapshotPreferences {
var delay: TimeInterval = 0
var precision: Float = 1
var perceptualPrecision: Float = 1
}
// MARK: - SnapshotTesting + Extensions
@@ -144,9 +167,7 @@ private extension PreviewDevice {
private extension Snapshotting where Value: SwiftUI.View, Format == UIImage {
static func prefireImage(drawHierarchyInKeyWindow: Bool = false,
precision: @escaping () -> Float,
perceptualPrecision: @escaping () -> Float,
duration: @escaping () -> TimeInterval,
preferences: SnapshotPreferences,
layout: SwiftUISnapshotLayout = .sizeThatFits,
traits: UITraitCollection = .init()) -> Snapshotting {
let config: ViewImageConfig
@@ -165,7 +186,7 @@ private extension Snapshotting where Value: SwiftUI.View, Format == UIImage {
config = .init(safeArea: .one, size: size, traits: traits)
}
return SimplySnapshotting<UIImage>(pathExtension: "png", diffing: .prefireImage(precision: precision, perceptualPrecision: perceptualPrecision, scale: traits.displayScale))
return SimplySnapshotting<UIImage>(pathExtension: "png", diffing: .prefireImage(preferences: preferences, scale: traits.displayScale))
.asyncPullback { view in
var config = config
@@ -181,31 +202,19 @@ private extension Snapshotting where Value: SwiftUI.View, Format == UIImage {
controller = hostingController
}
return Async<UIImage> { callback in
let strategy = snapshotView(config: config,
drawHierarchyInKeyWindow: drawHierarchyInKeyWindow,
traits: traits,
view: controller.view,
viewController: controller)
let duration = duration()
if duration != .zero {
let expectation = XCTestExpectation(description: "Wait")
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
expectation.fulfill()
}
_ = XCTWaiter.wait(for: [expectation], timeout: duration + 1)
}
strategy.run(callback)
}
return snapshotView(config: config,
drawHierarchyInKeyWindow: drawHierarchyInKeyWindow,
traits: traits,
view: controller.view,
viewController: controller)
}
}
}
private extension Diffing where Value == UIImage {
static func prefireImage(precision: @escaping () -> Float, perceptualPrecision: @escaping () -> Float, scale: CGFloat?) -> Diffing {
lazy var originalDiffing = Diffing.image(precision: precision(), perceptualPrecision: 0.98, scale: scale)
static func prefireImage(preferences: SnapshotPreferences, scale: CGFloat?) -> Diffing {
lazy var originalDiffing = Diffing.image(precision: preferences.precision, perceptualPrecision: preferences.perceptualPrecision, scale: scale)
return Diffing(toData: { originalDiffing.toData($0) },
fromData: { originalDiffing.fromData($0) },
diff: { originalDiffing.diff($0, $1) })
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b3181bdcf7d91c89da8af03b325c9ca5880c3e0a97c67bfb74da7c0c22048b19
size 36664
oid sha256:719a9f2d4a69ff30f6eedf146bb6cef2e79e4a6dabcdcc10a8429435531eb75d
size 362302
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0320762f030bc3ceddd164fb0fff5ad4743bbf2667039921a15a59bb2366b0a5
size 38589
oid sha256:7bd897cd6531b60437deda1a94f0a87c21e5ea33fdaf483eb699e99a4fd44908
size 365397
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:13e909870235a90145b7fc49967383e92a0bf45917f020cc793dcbde87fe2bfb
size 91414
oid sha256:656ab8534b3b28a11a09b7ee3bd735de4f9173687006d8346093ec8fa70caf54
size 88511
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a80a970d60708545d81c9c35be059484be1a6f686a53b3eafbdb831f5ef6729d
size 89319
oid sha256:6dece45267d9514a73241f045de87eb76d7d87ccc236aed3d114ccfbe9507d74
size 88837
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6136573195a4b3389a9d06966e5f0e9a400d6ea247b2ff5210dcb0e139a957b5
size 51143
oid sha256:d96438c624f5b1bcd690c82bacb133e08a830b0ee1a07f888fe845a60bfa1e6a
size 50998
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fca553c7fbb54deea774d261146a597ea426e00529a7a7cd7d64efabd8b7a1e6
size 178424
oid sha256:5090cca7e3b49782ad27006c8c6980f264f4e493fd8fa2b35c0a1ee6164b0b3a
size 178329
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:42b66da0630c9f320e7e1dc1b19261c647c5758e21248f61df5f134fc41e6201
size 51984
oid sha256:4c06f6941486a88c19d4b67d7b0afa91678649a535a30b696891fbb0815ac76b
size 61299
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c595691dab7881f5dfc46a5051d23fdcf7fb9684c7f1fbe445829fff02b9bbdd
size 181004
oid sha256:806e066b3632f2a73012c2bea9993f2f5026304235fd5ea072321a5f3db7e4f3
size 180923
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:305b1b6a9e5816174b64fd2819b79a7489405b0f7eef86d47b4471f6a117ba99
size 39207
oid sha256:0d7e551b2dfbec476b1164b49c8b800ab88f2fb3bf0e204f7fae38323ede91b4
size 17360
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:305b1b6a9e5816174b64fd2819b79a7489405b0f7eef86d47b4471f6a117ba99
size 39207
oid sha256:0d7e551b2dfbec476b1164b49c8b800ab88f2fb3bf0e204f7fae38323ede91b4
size 17360
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a4a39dd36ae824a1de6f05a815e3456f587c29f9b38e8241687d39af5de34aef
size 84853
oid sha256:1ff2b602ea177f6324527e5531720e718c36e922b7606c0bc67f8c5f8b6a9d23
size 93986
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5c9805e5c3f1fc3cf5182ba6dbe812bf85835c62b1caedeb2b8b9b96773e8c21
size 106502
oid sha256:8e69bbc84760736834b39a253232206e0f98942a9de5bb909b36ea80ffdee4d7
size 110395
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:222b8e57adaf6d234c2e6ac026cba0380131c7085d6a4d0e15e6fb349dbea1d8
size 93186
oid sha256:39c099e692cac9570866390ae3fa3e402cdd14f19de1df4ee63b206e84ac7e40
size 81301
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1040add34f78fa95ae396d7ce95df29a7f03c73ade149a5d0efa9418335531a6
size 95828
oid sha256:35bd1fba8ab30bb7d08080fe06cfadc8d0838a484bb9096c8cefd85b6af70543
size 85637
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fdaf986c1521ecb43b117f3197053f589e66e3e496a51d824629d8253653a713
size 49876
oid sha256:9658986db1f622979c311716b815190aa409fd70cd71d52ae9cedc5a2a2be60d
size 39632
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4840a11a65bc0a611c5cb2b93883cf707f1cbd686359909704398ed87eced86d
size 52399
oid sha256:73d404858351ed5066e5a5b9c78067c149cbf4f5b835d143172eb047e7bc7299
size 43558
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6f9871ae3306a42d01602c7a8cc2153131c1bd9c91f13129c3bdf0d8ece816c6
size 68400
oid sha256:9716ab5589ae525ee33f7c8d56c8de6d75eadbd58de55be381d7b6d1d2fa72d2
size 68373
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0a96b9608b3fbdef660995e45324eb1c5b5cbdf18cae679f17cded799aa9d1c9
size 48922
oid sha256:cc8c8a45449ae6a45e03cc55abbb7daacc73437723386278049702d075b2dfec
size 48907
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:58820cced75f459149aa85f2b32ddc9f6311fcb0795c5e8801b45c228751d698
size 83703
oid sha256:0cb4f935ad16ab0e44d65f0f3865865ea493be1c882ac6c9dfa0b9ef1ce2b7de
size 83685
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b022726222227a8f90998e54d58bcd7ab666089d1d7dff2724f21b11daf2b554
size 86214
oid sha256:8b7a2e7b7d6f76a6e2a99608791995c03b938c3157386ccb874e0351989be485
size 86192
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f7925fd0beeac60deea2db678b17265aedee906252d97d0214f641c2a21bf5fb
size 37037
oid sha256:5b50eec37266cca3aaf324a6b477aba2c0e10ac37540ace2f84d9cfcc9b914c4
size 77856
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:de46b50e23ecb747e9d60c9448b60a977571af8e74ce4f5ca20e440189ce36f7
size 73574
oid sha256:9d2c1fd3265cdb97ffe95cf0fd795bc0751c86de6d1e6f5228000d8daddbffb3
size 73543
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9d8830e48e24781b958a21e9ce56ac7f12583cf237fc4a40cff57a4a8fb4e5e2
size 59454
oid sha256:4f81f36ffc30df200c36a8bb8c47e47564951262079850d3f3baffdac96a61a2
size 59461
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:54a2cde62d75459851c485b761aa5161cbb4176560eeaab69fcfa520e362bb11
size 93420
oid sha256:65d99cd60b4113ae8d91b8602b39800ea2643160952f1c5d59f9dd28628ea9b2
size 93372
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f4853074c004e451d4d88453b977064c25b59a29803acb1ea5b38882c7860ad3
size 96319
oid sha256:e1487324353bcdd77881ba9ca264a05d7a932bb857d4f61accba7c28ea65d871
size 96301
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4758385db96e9d880f7ff670f1b312964b27910ba1c55ecafa9a451c964ab474
size 40619
oid sha256:027180a5377922209a9b58f36c2ab352ae5f58dad3fa653982276ce07d155d38
size 86305
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ce5e1bd5e6f7325fdea0332cce899a8f0c1439d971d159324d48f89ed3a58ac6
size 106225
oid sha256:1062bd3adf6d606bb0d1f8c693bff1e66ae6110e0bf5a6f65037f2b980e6e39e
size 111198
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3755ae2d427b03ba0f591ac1358c8e74c14cb378a20a53560e342b6077562837
size 123000
oid sha256:1a3e452e09cb3ab4908ad93c1802ceae66dc60334a2494fce8c34ea58bc2ab7e
size 127950
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8ba473ce453cdd3f505411fb9cfcafb8409d073d0c737ce3716f2c251acb0bf0
size 494165
oid sha256:03de5c32a0b0e47ea2b2d54214048fa1f9dadd7a7bed970af935d1c1ca0cf7ce
size 445294
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8ba473ce453cdd3f505411fb9cfcafb8409d073d0c737ce3716f2c251acb0bf0
size 494165
oid sha256:03de5c32a0b0e47ea2b2d54214048fa1f9dadd7a7bed970af935d1c1ca0cf7ce
size 445294
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e8a1c81979803d04546d37f153017199c560cffe91f12d132449784849f00903
size 69227
oid sha256:f8122c559a90fd4425a7117960fad021482fa2c93585a72f6f2814d619d7bf2d
size 69384
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:69d3cf0672978bc46113354cb6b7ead0b815f1db78a51d1e355a2d7c06fcc7cc
size 70107
oid sha256:f0f1970da3feb417beeb4a007406510a33a8d204fece27109d883b1b1bb76ea7
size 70243
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a319198ac7b9f91e4ed6bd9411a73fd9b7cabf80483507a4cf975426305379b2
size 71910
oid sha256:641bf4dd2d18f7ff9f72d9aadf574fc276deda99525cd6c933506043c60a6856
size 71904
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a319198ac7b9f91e4ed6bd9411a73fd9b7cabf80483507a4cf975426305379b2
size 71910
oid sha256:641bf4dd2d18f7ff9f72d9aadf574fc276deda99525cd6c933506043c60a6856
size 71904
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7fe33b9011954c24dfe13514927cedd7b076f43690f163a13b537a80f998fc8c
size 31324
oid sha256:31b749bf22b17db53d6a0128b366a73238ee89e7593c71c445e911608835a588
size 31069
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7fe33b9011954c24dfe13514927cedd7b076f43690f163a13b537a80f998fc8c
size 31324
oid sha256:31b749bf22b17db53d6a0128b366a73238ee89e7593c71c445e911608835a588
size 31069
@@ -113,11 +113,11 @@ class ComposerToolbarViewModelTests: XCTestCase {
XCTAssertEqual(viewModel.state.suggestions, suggestions)
}
func testSuggestionTrigger() async {
func testSuggestionTrigger() async throws {
let deferred = deferFulfillment(wysiwygViewModel.$attributedContent) { $0.plainText == "#not_implemented_yay" }
wysiwygViewModel.setMarkdownContent("@test")
wysiwygViewModel.setMarkdownContent("#not_implemented_yay")
await Task.yield()
try await deferred.fulfill()
// The first one is nil because when initialised the view model is empty
XCTAssertEqual(completionSuggestionServiceMock.setSuggestionTriggerReceivedInvocations, [nil, .init(type: .user, text: "test", range: .init(location: 0, length: 5)), nil])
@@ -256,39 +256,6 @@ class TimelineViewModelTests: XCTestCase {
XCTAssertEqual(arguments?.type, .read)
}
func testSendMoreReadReceipts() async throws {
// Given a room with only text items in the timeline that are all read.
let items = [TextRoomTimelineItem(eventID: "t1"),
TextRoomTimelineItem(eventID: "t2"),
TextRoomTimelineItem(eventID: "t3")]
let (viewModel, _, timelineProxy, timelineController) = readReceiptsConfiguration(with: items)
viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.last!.id))
try await Task.sleep(for: .milliseconds(100))
XCTAssertEqual(timelineProxy.sendReadReceiptForTypeCallsCount, 1)
var arguments = timelineProxy.sendReadReceiptForTypeReceivedArguments
XCTAssertEqual(arguments?.eventID, "t3")
XCTAssertEqual(arguments?.type, .read)
// When sending a receipt for the first item in the timeline.
viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(items.first!.id))
try await Task.sleep(for: .milliseconds(100))
// When a new message is received and marked as read.
let newMessage = TextRoomTimelineItem(eventID: "t4")
timelineController.timelineItems.append(newMessage)
timelineController.callbacks.send(.updatedTimelineItems(timelineItems: timelineController.timelineItems, isSwitchingTimelines: false))
try await Task.sleep(for: .milliseconds(100))
viewModel.context.send(viewAction: .sendReadReceiptIfNeeded(newMessage.id))
try await Task.sleep(for: .milliseconds(100))
// Then the request should be made.
XCTAssertEqual(timelineProxy.sendReadReceiptForTypeCallsCount, 3)
arguments = timelineProxy.sendReadReceiptForTypeReceivedArguments
XCTAssertEqual(arguments?.eventID, "t4")
XCTAssertEqual(arguments?.type, .read)
}
func testSendReadReceiptWithoutEvents() async throws {
// Given a room with only virtual items.
let items = [SeparatorRoomTimelineItem(timelineID: "v1"),
@@ -246,7 +246,7 @@ class UserSessionFlowCoordinatorTests: XCTestCase {
private func process(route: AppRoute, expectedState: UserSessionFlowCoordinatorStateMachine.State) async throws {
// Sometimes the state machine's state changes before the coordinators have updated the stack.
let delayedPublisher = userSessionFlowCoordinator.statePublisher.delay(for: .milliseconds(10), scheduler: DispatchQueue.main)
let delayedPublisher = userSessionFlowCoordinator.statePublisher.delay(for: .milliseconds(100), scheduler: DispatchQueue.main)
let deferred = deferFulfillment(delayedPublisher) { $0 == expectedState }
userSessionFlowCoordinator.handleAppRoute(route, animated: true)