Timeline UI Tests (#380)

* Add signalling between UI tests and the app.

* Fix navigationStack on roomLayout screens and update screenshots

Co-authored-by: Stefan Ceriu <stefanc@matrix.org>
This commit is contained in:
Doug
2023-01-05 16:35:20 +00:00
committed by GitHub
parent 530ae336ab
commit 51e300642c
63 changed files with 597 additions and 79 deletions
+8 -2
View File
@@ -173,6 +173,7 @@
5B8B51CEC4717AF487794685 /* NotificationServiceProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B490675B8E31423AF116BDA /* NotificationServiceProxy.swift */; };
5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C830A64609CBD152F06E0457 /* NotificationConstants.swift */; };
5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */; };
5D1C6D4E3583700DF7A64834 /* UITestSignalling.swift in Sources */ = {isa = PBXBuildFile; fileRef = D619626288AAB513B7620BB6 /* UITestSignalling.swift */; };
5D2AF8C0DF872E7985F8FE54 /* TimelineDeliveryStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5AC06FC11B6638F7BF1670E /* TimelineDeliveryStatusView.swift */; };
5D430CDE11EAC3E8E6B80A66 /* RoomTimelineViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FEE631F3A4AFDC6652DD9DA /* RoomTimelineViewFactory.swift */; };
5D70FAE4D2BF4553AFFFFE41 /* NotificationItemProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */; };
@@ -459,6 +460,7 @@
F6E860FF7B18B81DF43B30B8 /* EncryptedRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3FA7C8D4EF2B1873C180ED7 /* EncryptedRoomTimelineItem.swift */; };
F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */; };
F7567DD6635434E8C563BF85 /* AnalyticsClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */; };
F98333D53D85B0E029FDCA12 /* UITestSignalling.swift in Sources */ = {isa = PBXBuildFile; fileRef = D619626288AAB513B7620BB6 /* UITestSignalling.swift */; };
F9981191DC408AED537C1749 /* MediaProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12C9E0B61A77C7F0EE7918C /* MediaProxy.swift */; };
F99FB21EFC6D99D247FE7CBE /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DE8DC9B3FBA402117DC4C49F /* Kingfisher */; };
F9F6D2883BBEBB9A3789A137 /* OnboardingViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00A941F289F6AB876BA3361A /* OnboardingViewModelTests.swift */; };
@@ -708,7 +710,6 @@
541542F5AC323709D8563458 /* AnalyticsPrompt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPrompt.swift; sourceTree = "<group>"; };
542D4F49FABA056DEEEB3400 /* RustTracing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RustTracing.swift; sourceTree = "<group>"; };
5445FCE0CE15E634FDC1A2E2 /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = "<group>"; };
551DAED7F623AA5366E79927 /* repository */ = {isa = PBXFileReference; lastKnownFileType = folder; name = repository; path = .; sourceTree = SOURCE_ROOT; };
55BC11560C8A2598964FFA4C /* bs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bs; path = bs.lproj/Localizable.strings; sourceTree = "<group>"; };
55D7187F6B0C0A651AC3DFFA /* in */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = in; path = in.lproj/Localizable.strings; sourceTree = "<group>"; };
55EA4B03F92F31EAA83B3F7B /* FilePreviewModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreviewModels.swift; sourceTree = "<group>"; };
@@ -953,10 +954,12 @@
D1A9CCCF53495CF3D7B19FCE /* MockSessionVerificationControllerProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSessionVerificationControllerProxy.swift; sourceTree = "<group>"; };
D263254AFE5B7993FFBBF324 /* NSE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NSE.entitlements; sourceTree = "<group>"; };
D2D783758EAE6A88C93564EB /* VideoPlayerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerViewModel.swift; sourceTree = "<group>"; };
D31DC8105C6233E5FFD9B84C /* element-x-ios */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "element-x-ios"; path = .; sourceTree = SOURCE_ROOT; };
D33116993D54FADC0C721C1F /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
D3D455BC2423D911A62ACFB2 /* NSELogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSELogger.swift; sourceTree = "<group>"; };
D4DA544B2520BFA65D6DB4BB /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; };
D5AC06FC11B6638F7BF1670E /* TimelineDeliveryStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineDeliveryStatusView.swift; sourceTree = "<group>"; };
D619626288AAB513B7620BB6 /* UITestSignalling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestSignalling.swift; sourceTree = "<group>"; };
D653265D006E708E4E51AD64 /* HomeScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenCoordinator.swift; sourceTree = "<group>"; };
D67CBAFA48ED0B6FCE74F88F /* lt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = lt; path = lt.lproj/Localizable.strings; sourceTree = "<group>"; };
D6CA5F386C7701C129398945 /* AuthenticationCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationCoordinator.swift; sourceTree = "<group>"; };
@@ -1254,6 +1257,7 @@
children = (
46C208DA43CE25D13E670F40 /* UITestsAppCoordinator.swift */,
CC6FE34A0A47D010BBB4D4D4 /* UITestScreenIdentifier.swift */,
D619626288AAB513B7620BB6 /* UITestSignalling.swift */,
D751BB69BB7C38FD247517B4 /* UITestsRootCoordinator.swift */,
);
path = UITests;
@@ -1937,7 +1941,7 @@
9413F680ECDFB2B0DDB0DEF2 /* Packages */ = {
isa = PBXGroup;
children = (
551DAED7F623AA5366E79927 /* repository */,
D31DC8105C6233E5FFD9B84C /* element-x-ios */,
);
name = Packages;
sourceTree = SOURCE_ROOT;
@@ -3164,6 +3168,7 @@
706F79A39BDB32F592B8C2C7 /* UIKitBackgroundTask.swift in Sources */,
3097A0A867D2B19CE32DAE58 /* UIKitBackgroundTaskService.swift in Sources */,
D05A193AE63030F2CFCE2E9C /* UITestScreenIdentifier.swift in Sources */,
5D1C6D4E3583700DF7A64834 /* UITestSignalling.swift in Sources */,
E96005321849DBD7C72A28F2 /* UITestsAppCoordinator.swift in Sources */,
086C2FA7750378EB2BFD0BEE /* UITestsRootCoordinator.swift in Sources */,
071A017E415AD378F2961B11 /* URL.swift in Sources */,
@@ -3221,6 +3226,7 @@
C4180F418235DAD9DD173951 /* TemplateScreenUITests.swift in Sources */,
0ED951768EC443A8728DE1D7 /* TimelineStyle.swift in Sources */,
9A47B7EFE3793760EEF68FFE /* UITestScreenIdentifier.swift in Sources */,
F98333D53D85B0E029FDCA12 /* UITestSignalling.swift in Sources */,
35C57543D245E82CBFE15DF0 /* URL.swift in Sources */,
B22D857D1E8FCA6DD74A58E3 /* UserSessionScreenTests.swift in Sources */,
588411C8FD72B2A2DFE5F7DE /* XCUIElement.swift in Sources */,
@@ -79,4 +79,6 @@ struct RoomScreenViewStateBindings {
enum RoomScreenErrorType: Hashable {
/// A specific error message shown in an alert.
case alert(String)
/// A specific error message shown in a toast.
case toast(String)
}
@@ -22,6 +22,7 @@ typealias RoomScreenViewModelType = StateStoreViewModel<RoomScreenViewState, Roo
class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol {
private enum Constants {
static let backPaginationPageSize: UInt = 50
static let toastErrorID = "RoomScreenToastError"
}
private let timelineController: RoomTimelineControllerProtocol
@@ -123,7 +124,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
private func paginateBackwards() async {
switch await timelineController.paginateBackwards(Constants.backPaginationPageSize) {
case .failure:
displayError(.alert(ElementL10n.roomTimelineBackpaginationFailure))
displayError(.toast(ElementL10n.roomTimelineBackpaginationFailure))
default:
break
}
@@ -179,6 +180,11 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
state.bindings.alertInfo = AlertInfo(id: type,
title: ElementL10n.dialogTitleError,
message: message)
case .toast(let message):
ServiceLocator.shared.userNotificationController.submitNotification(UserNotification(id: Constants.toastErrorID,
type: .toast,
title: message,
iconName: "xmark"))
}
}
@@ -42,6 +42,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
styledContentWithReactions
}
.padding(.horizontal, 16.0)
.padding(timelineItem.isOutgoing ? .leading : .trailing, 40) // Extra padding to differentiate alignment.
}
if timelineItem.isOutgoing {
@@ -64,7 +64,7 @@ enum RoomTimelineItemFixtures {
senderId: "",
senderDisplayName: "Helena"),
TextRoomTimelineItem(id: UUID().uuidString,
text: "And John's speech was amazing!And John's speech was amazing!And John's speech was amazing!",
text: "And John's speech was amazing!",
timestamp: "5 PM",
inGroupState: .beginning,
isOutgoing: true,
@@ -20,13 +20,8 @@ import Foundation
class MockRoomTimelineController: RoomTimelineControllerProtocol {
/// An array of timeline item arrays that will be inserted in order for each back pagination request.
var backPaginationResponses: [[RoomTimelineItemProtocol]] = []
/// The time delay added to each back pagination request.
var backPaginationDelay: Duration = .milliseconds(500)
/// An array of timeline items that will be appended in order when ``simulateIncomingItems()`` is called.
var incomingItems: [RoomTimelineItemProtocol] = []
/// The time delay between each incoming item.
var incomingDelay: Duration = .milliseconds(750)
let roomId = "MockRoomIdentifier"
@@ -34,38 +29,17 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
var timelineItems: [RoomTimelineItemProtocol] = RoomTimelineItemFixtures.default
func simulateIncomingItems() {
guard !incomingItems.isEmpty else { return }
let incomingItem = incomingItems.removeFirst()
Task {
try await Task.sleep(for: incomingDelay)
timelineItems.append(incomingItem)
callbacks.send(.updatedTimelineItems)
if !self.incomingItems.isEmpty {
simulateIncomingItems()
}
private var client: UITestsSignalling.Client?
init(listenForSignals: Bool = false) {
if listenForSignals {
client = .init()
Task { try await startListening() }
}
}
func paginateBackwards(_ count: UInt) async -> Result<Void, RoomTimelineControllerError> {
callbacks.send(.startedBackPaginating)
guard !backPaginationResponses.isEmpty else {
callbacks.send(.finishedBackPaginating)
return .failure(.generic)
}
let newItems = backPaginationResponses.removeFirst()
try? await Task.sleep(for: backPaginationDelay)
timelineItems.insert(contentsOf: newItems, at: 0)
callbacks.send(.updatedTimelineItems)
callbacks.send(.finishedBackPaginating)
return .success(())
.failure(.generic)
}
func processItemAppearance(_ itemId: String) async { }
@@ -89,4 +63,57 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
}
func retryDecryption(forSessionId sessionId: String) async { }
// MARK: - UI Test signalling
/// Allows the simulation of server responses by listening for signals from UI tests.
private func startListening() async throws {
try await client?.connect()
Task {
while let client {
do {
try await handleSignal(client.receive())
} catch {
client.disconnect()
self.client = nil
}
}
}
}
/// Handles a UI test signal as necessary.
private func handleSignal(_ signal: UITestsSignal) async throws {
switch signal {
case .paginate:
try await simulateBackPagination()
case .incomingMessage:
try await simulateIncomingItem()
default:
break
}
}
/// Appends the next incoming item to the `timelineItems` array.
private func simulateIncomingItem() async throws {
guard !incomingItems.isEmpty else { return }
let incomingItem = incomingItems.removeFirst()
timelineItems.append(incomingItem)
callbacks.send(.updatedTimelineItems)
try? await client?.send(.success)
}
/// Prepends the next chunk of items to the `timelineItems` array.
private func simulateBackPagination() async throws {
guard !backPaginationResponses.isEmpty else { return }
let newItems = backPaginationResponses.removeFirst()
timelineItems.insert(contentsOf: newItems, at: 0)
callbacks.send(.updatedTimelineItems)
callbacks.send(.finishedBackPaginating)
try? await client?.send(.success)
}
}
@@ -35,6 +35,9 @@ enum UITestScreenIdentifier: String {
case roomSmallTimeline
case roomSmallTimelineIncomingAndSmallPagination
case roomSmallTimelineLargePagination
case roomLayoutTop
case roomLayoutMiddle
case roomLayoutBottom
case sessionVerification
case userSessionScreen
case roomDetailsScreen
@@ -0,0 +1,191 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import Network
enum UITestsSignal: String {
/// An internal signal used to bring up the connection.
case connect
/// Ask the app to back paginate.
case paginate
/// Ask the app to simulate an incoming message.
case incomingMessage
/// The operation has completed successfully.
case success
}
enum UITestsSignalError: Error {
/// An unknown error occurred.
case unknown
/// Signalling could not be used as is hasn't been enabled.
case disabled
/// The connection was cancelled.
case cancelled
/// The connection hasn't been established.
case notConnected
/// Attempted to receive multiple signals at once.
case awaitingAnotherSignal
/// A network error occurred.
case nwError(NWError)
/// An unexpected signal was received. This error isn't used internally.
case unexpected
}
enum UITestsSignalling {
/// A UDP server that can be used for signalling between the UI tests jig and the app.
/// The server should be instantiated on the UI tests side.
class Server: UITestsSignallingProtocol {
private let listener: NWListener
var connection: NWConnection
var nextMessageContinuation: CheckedContinuation<UITestsSignal, Error>?
/// Creates a new signalling server.
init() throws {
connection = NWConnection(host: "127.0.0.1", port: 0, using: .udp)
listener = try NWListener(using: .udp, on: 1234)
}
/// Listens for a client and attempts to negotiate a connection when one is discovered.
func connect() async throws {
guard listener.state == .setup else { return }
return try await withCheckedThrowingContinuation { [weak self] continuation in
listener.newConnectionHandler = { connection in
connection.start(queue: .main)
connection.stateUpdateHandler = { state in
switch state {
case .ready:
self?.listener.cancel()
self?.receiveNextMessage()
continuation.resume()
case .failed(let error):
self?.listener.cancel()
continuation.resume(with: .failure(error))
default:
break
}
}
self?.connection = connection
}
listener.start(queue: .main)
}
}
/// Stops the connection (or the listener if a connection hasn't been established).
func disconnect() {
listener.cancel()
connection.cancel()
if let nextMessageContinuation {
nextMessageContinuation.resume(throwing: UITestsSignalError.cancelled)
self.nextMessageContinuation = nil
}
}
}
/// A UDP client that can be used for signalling between the app and the UI tests jig.
/// The client should be instantiated on the app side.
class Client: UITestsSignallingProtocol {
let connection: NWConnection
var nextMessageContinuation: CheckedContinuation<UITestsSignal, Error>?
/// Creates a new signalling client.
init() {
connection = NWConnection(host: "127.0.0.1", port: 1234, using: .udp)
}
/// Attempts to connect to a server.
func connect() async throws {
guard connection.state == .setup else { return }
return try await withCheckedThrowingContinuation { continuation in
connection.start(queue: .main)
connection.stateUpdateHandler = { state in
switch state {
case .ready:
self.receiveNextMessage()
continuation.resume()
Task { try await self.send(.connect) }
case .failed(let error):
continuation.resume(with: .failure(error))
default:
break
}
}
}
}
/// Stops the connection.
func disconnect() {
connection.cancel()
if let nextMessageContinuation {
nextMessageContinuation.resume(throwing: UITestsSignalError.cancelled)
self.nextMessageContinuation = nil
}
}
}
}
/// A shared implementation for sending/receiving signals between a client and a server.
protocol UITestsSignallingProtocol: AnyObject {
var connection: NWConnection { get }
var nextMessageContinuation: CheckedContinuation<UITestsSignal, Error>? { get set }
}
extension UITestsSignallingProtocol {
/// Sends a message to the connected client/server.
func send(_ signal: UITestsSignal) async throws {
guard connection.state == .ready else { throw UITestsSignalError.notConnected }
let data = signal.rawValue.data(using: .utf8)
connection.send(content: data, completion: .idempotent)
}
/// Returns the next message received by the client/server.
func receive() async throws -> UITestsSignal {
guard connection.state == .ready else { throw UITestsSignalError.notConnected }
guard nextMessageContinuation == nil else { throw UITestsSignalError.awaitingAnotherSignal }
return try await withCheckedThrowingContinuation { continuation in
self.nextMessageContinuation = continuation
}
}
/// Processes the next message received by the client/server
fileprivate func receiveNextMessage() {
connection.receiveMessage { [weak self] completeContent, _, isComplete, error in
guard let self else { return }
guard isComplete else { fatalError("Partial messages not supported") }
guard let completeContent,
let message = String(data: completeContent, encoding: .utf8),
let signal = UITestsSignal(rawValue: message)
else {
let error: UITestsSignalError = error.map { .nwError($0) } ?? .unknown
self.nextMessageContinuation?.resume(with: .failure(error))
self.nextMessageContinuation = nil
return
}
if signal != .connect {
self.nextMessageContinuation?.resume(returning: signal)
self.nextMessageContinuation = nil
}
self.receiveNextMessage()
}
}
}
@@ -178,11 +178,10 @@ class MockScreen: Identifiable {
case .roomSmallTimelineIncomingAndSmallPagination:
let navigationStackCoordinator = NavigationStackCoordinator()
let timelineController = MockRoomTimelineController()
let timelineController = MockRoomTimelineController(listenForSignals: true)
timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk
timelineController.backPaginationResponses = [RoomTimelineItemFixtures.singleMessageChunk]
timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage]
timelineController.simulateIncomingItems()
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
roomProxy: MockRoomProxy(displayName: "Small timeline", avatarURL: "mock_url"),
timelineController: timelineController,
@@ -195,7 +194,7 @@ class MockScreen: Identifiable {
case .roomSmallTimelineLargePagination:
let navigationStackCoordinator = NavigationStackCoordinator()
let timelineController = MockRoomTimelineController()
let timelineController = MockRoomTimelineController(listenForSignals: true)
timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk
timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk]
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
@@ -205,6 +204,52 @@ class MockScreen: Identifiable {
emojiProvider: EmojiProvider())
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomLayoutTop:
let navigationStackCoordinator = NavigationStackCoordinator()
let timelineController = MockRoomTimelineController(listenForSignals: true)
timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk
timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk]
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
roomProxy: MockRoomProxy(displayName: "Large timeline", avatarURL: "mock_url"),
timelineController: timelineController,
mediaProvider: MockMediaProvider(),
emojiProvider: EmojiProvider())
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomLayoutMiddle:
let navigationStackCoordinator = NavigationStackCoordinator()
let timelineController = MockRoomTimelineController(listenForSignals: true)
timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk
timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk]
timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage]
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
roomProxy: MockRoomProxy(displayName: "Large timeline", avatarURL: "mock_url"),
timelineController: timelineController,
mediaProvider: MockMediaProvider(),
emojiProvider: EmojiProvider())
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomLayoutBottom:
let navigationStackCoordinator = NavigationStackCoordinator()
let timelineController = MockRoomTimelineController(listenForSignals: true)
timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk
timelineController.incomingItems = [RoomTimelineItemFixtures.incomingMessage]
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
roomProxy: MockRoomProxy(displayName: "Large timeline", avatarURL: "mock_url"),
timelineController: timelineController,
mediaProvider: MockMediaProvider(),
emojiProvider: EmojiProvider())
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .sessionVerification:
+22 -2
View File
@@ -41,13 +41,21 @@ extension XCUIApplication {
/// Assert screenshot for a screen with the given identifier. Does not fail if a screenshot is newly created.
/// - Parameter identifier: Identifier of the UI test screen
func assertScreenshot(_ identifier: UITestScreenIdentifier, step: Int? = nil) {
/// - Parameter step: An optional integer that can be used to take multiple snapshots per test identifier.
/// - Parameter insets: Optional insets with which to crop the image by.
func assertScreenshot(_ identifier: UITestScreenIdentifier, step: Int? = nil, insets: UIEdgeInsets? = nil) {
var snapshotName = identifier.rawValue
if let step {
snapshotName += "-\(step)"
}
var snapshot = XCUIScreen.main.screenshot().image
if let insets {
snapshot = snapshot.inset(by: insets)
}
let failure = verifySnapshot(matching: XCUIScreen.main.screenshot().image,
let failure = verifySnapshot(matching: snapshot,
as: .image(precision: 0.99, perceptualPrecision: 0.98, scale: nil),
named: snapshotName,
testName: testName)
@@ -79,3 +87,15 @@ extension XCUIApplication {
UIDevice.current.systemVersion.replacingOccurrences(of: ".", with: "-")
}
}
private extension UIImage {
/// Adjusts the image by cropping it with the given edge insets.
func inset(by insets: UIEdgeInsets) -> UIImage {
let insetRect = CGRect(origin: .zero, size: size).inset(by: insets)
let renderer = UIGraphicsImageRenderer(size: insetRect.size)
return renderer.image { _ in
draw(at: CGPoint(x: -insets.left, y: -insets.top))
}
}
}
+115 -7
View File
@@ -47,19 +47,127 @@ class RoomScreenUITests: XCTestCase {
app.assertScreenshot(.roomSmallTimeline)
}
func testSmallTimelineWithIncomingAndPagination() {
func testSmallTimelineWithIncomingAndPagination() async throws {
let server = try UITestsSignalling.Server()
let app = Application.launch()
app.goToScreenWithIdentifier(.roomSmallTimelineIncomingAndSmallPagination)
// Wait for both the incoming message and the pagination chunk.
XCTAssert(app.staticTexts["Bob"].waitForExistence(timeout: 2))
XCTAssert(app.staticTexts["Helena"].waitForExistence(timeout: 2))
try await server.connect()
defer { server.disconnect() }
// When a back pagination occurs and an incoming message arrives.
try await performOperation(.incomingMessage, using: server)
try await performOperation(.paginate, using: server)
// The messages should still be bottom aligned after the new items are added.
// Then the 4 visible messages should stay aligned to the bottom.
app.assertScreenshot(.roomSmallTimelineIncomingAndSmallPagination)
}
func testSmallTimelineWithLargePagination() {
// To be implemented
func testSmallTimelineWithLargePagination() async throws {
let server = try UITestsSignalling.Server()
let app = Application.launch()
app.goToScreenWithIdentifier(.roomSmallTimelineLargePagination)
try await server.connect()
defer { server.disconnect() }
// When a large back pagination occurs.
try await performOperation(.paginate, using: server)
// The bottom of the timeline should remain visible with more items added above.
app.assertScreenshot(.roomSmallTimelineLargePagination)
}
func testTimelineLayoutInMiddle() async throws {
let server = try UITestsSignalling.Server()
let app = Application.launch()
app.goToScreenWithIdentifier(.roomLayoutMiddle)
try await server.connect()
defer { server.disconnect() }
// Given a timeline that is neither at the top nor the bottom.
app.tables.element.swipeDown()
try await Task.sleep(for: .milliseconds(500)) // Allow the table to settle
app.assertScreenshot(.roomLayoutMiddle, step: 0) // Assert initial state for comparison.
// When a back pagination occurs.
try await performOperation(.paginate, using: server)
// Then the UI should remain unchanged.
app.assertScreenshot(.roomLayoutMiddle, step: 0)
// When an incoming message arrives
try await performOperation(.incomingMessage, using: server)
// Then the UI should still remain unchanged.
app.assertScreenshot(.roomLayoutMiddle, step: 0)
// When the keyboard appears for the message composer.
try await tapMessageComposer(in: app)
// Then the timeline scroll offset should remain unchanged.
app.assertScreenshot(.roomLayoutMiddle, step: 1)
}
func testTimelineLayoutAtTop() async throws {
let server = try UITestsSignalling.Server()
let app = Application.launch()
app.goToScreenWithIdentifier(.roomLayoutTop)
try await server.connect()
defer { server.disconnect() }
// Given a timeline that is scrolled to the top.
while !app.staticTexts["Bacon ipsum dolor amet commodo incididunt ribeye dolore cupidatat short ribs."].isHittable {
app.tables.element.swipeDown()
}
let cropped = UIEdgeInsets(top: 150, left: 0, bottom: 0, right: 0) // Ignore the navigation bar and pagination indicator as these change.
app.assertScreenshot(.roomLayoutTop, insets: cropped) // Assert initial state for comparison.
// When a back pagination occurs.
try await performOperation(.paginate, using: server)
// Then the bottom of the timeline should remain unchanged (with new items having been added above).
app.assertScreenshot(.roomLayoutTop, insets: cropped)
}
func testTimelineLayoutAtBottom() async throws {
let server = try UITestsSignalling.Server()
let app = Application.launch()
app.goToScreenWithIdentifier(.roomLayoutBottom)
try await server.connect()
defer { server.disconnect() }
// When an incoming message arrives.
try await performOperation(.incomingMessage, using: server)
// Then the timeline should scroll down to reveal the message.
app.assertScreenshot(.roomLayoutBottom, step: 0)
// When the keyboard appears for the message composer.
try await tapMessageComposer(in: app)
// Then the timeline should still show the last message.
app.assertScreenshot(.roomLayoutBottom, step: 1)
}
// MARK: - Helper Methods
private func performOperation(_ operation: UITestsSignal, using server: UITestsSignalling.Server) async throws {
try await server.send(operation)
guard try await server.receive() == .success else { throw UITestsSignalError.unexpected }
try await Task.sleep(for: .milliseconds(500)) // Allow the timeline to update
}
private func tapMessageComposer(in app: XCUIApplication) async throws {
app.textViews.element.tap()
try await Task.sleep(for: .milliseconds(500)) // Allow the animations to complete
}
}
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9902bf55f027c729fd31b790d875699bf4a2e0f2852b9ceb2178c84bc8aca226
size 211684
oid sha256:32e747c2a8b7b7cd5aeee2f8e50f26ef2af0bf03d2fd4a89e25fad0104687fcf
size 211496
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3cb8e1d07e418e7a3fecd9b0699158b8fef40609771fef9666428015d056738c
size 305317
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:410e331dca5dbd99356ed345216f83288e231b808d4c3cbc05276a38dc5fba1f
size 304826
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6018b19cf7b412b3a74080a4b3b4bf35ce86a4ef187f9b4945f68b9744760dd6
size 314107
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5d507e3246043e67cc67873a83f9cb2a82e200f6debecfbf2cc4dd2e1fab86e9
size 321969
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6aef1da13173322dbd51e933df69733a48c02b89cfdf564c41ac1933c7a4f9b8
size 303249
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1fbf57e59072a7e06026a69a0858ed9f65a72e0bfa4fabdf4de6a28e1bc4b364
size 211052
oid sha256:85656b18c4ef17cf3a36aaef699fffdd34c43089749b5496b09f18fd3a520e60
size 210875
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:089f14916f6e386fb8ea620df10e74f8f24c703ccfba65b2c46c5f5888c79f3d
size 311767
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dd603c2e38d5281190788cfa06425a512587fbb49a32d5d895361533ab48419a
size 373824
oid sha256:9ba4ef4f768e1c44c0e058db23dc97e8a50516af2a2001fc5f433abf16aeac1a
size 366658
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8943c20d22d19e720eb949a47dce8ab304da3cdb6ae292dc007d38c62ee435a8
size 330436
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4312ba6002da73071187b871f6532f122bb6c4c3fdf3c9e7a93e9baffd27edfd
size 323699
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ff71a79bf353a284c2c8afa92ce2562a45c25c64486fb132d795a07bf0c4b6f0
size 335617
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a466639c4f320f4d684fe485255fd217d960665d8d9ea6cd1dd0e1f425b8cac0
size 335663
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:01a5fa9431bdfa0dffc6ed4ec5816db6eb923b8048f3af3124eb8701dad59ba1
size 283965
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:066c66947a2d47092dde8db56d885cd62127f24a817a06198e2ab0d29fa61857
size 371508
oid sha256:54301aeca9fdaea001bbabf7bd9d4624c820c6cca08bd75eaa3a66a3b88e271a
size 365082
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3295b6e8348a37998c95f72285ae0bfc5651ec3166a376c327f482a0baa1e93d
size 114678
oid sha256:94c4a7bb1f6ecf74120324ab118ab314f07663d0b01889581804afc6826a70d0
size 115882
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3f7192b4cffb7a9079b4a90a4dcb03d663cde46c0835eeb2481deead9d069d30
size 333980
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0a34acc9936b4fbff9aad17617ee77dd8c89356d652b2a7132f5bb7d20c4f715
size 210981
oid sha256:874b7aafe67bc385648ffdfce0d6fa8b75899dcd923de24a97b2d9a5246d4e9e
size 210847
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e293cc224e3c71f5339ca59f10db4450d96e025738c9117b18d41b57fe49a4d5
size 305117
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4580e0f596f44aece15bf7b8deb28b996e4c3f2132babd243d8896cdc67319f9
size 307638
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f5bf2982f09c57a980df0dc3d3dba3d3bafa67269704aa342a2dd5d169e9971b
size 313966
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d9ae251abae3f358ac0c2aae41ff55ff9eb209696d2dff43154679cfb2ba30ea
size 319312
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3e2192501b67459b5d4f85353976e7d3a7b5cf55d5b1f712b41c736579a919be
size 303063
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ec8c0bdbf78d2bd1019cd8319aed419fd729be3d8be566b729d171cc21adec8b
size 210340
oid sha256:e181e9ded4026b2ecfb48c7a2f9d3f4b5045c4d08608dbca45ad26cca831c160
size 210222
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c7482b45bcde59ec93b1a3f211d2f3187357b5326be5a70dce4a309c8204b6a2
size 311570
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f7322585bdb968fa0ad5244e5ca05401e7b32e3c27a0e3b1abb4f74f2cc736f9
size 372873
oid sha256:aa7c6d90c7f8021a1131a5c5db0f15aaf282548642cd97aeecac8574969771d3
size 366679
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ba8df321653a203447b6a48facc32c1b99a3167ed3f1c44019d06aa53b07db9b
size 330472
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:611e841427673aee43571e8b3079f9f84f9624b05efcc678f687ec496c9d66af
size 323568
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e17cbe7c08f8feddfeaec82d64d4c7ba001c91ef3ee5a38efd4e9eafe73e9d9b
size 335662
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:90f0175794dfebcc609105207e345e34c8d272cbd896f4bff0786ee4ecbf9c0c
size 335528
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e9b41ae6543ffcc994c2468f48641149d1932065600f94af29379d0fbb5d1297
size 283690
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bf8edaf502cb0a8ea89699280a31ef2a8505b2107fb67d81975b9a63813bdd2e
size 370584
oid sha256:92e93f947f47be4f87e3c997f1e9103064771a78a95836dc38b24ed92431d581
size 365103
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a365ef80c8bce284dfddd840433afe9fa6b3d7eee8521da98e065777a6819530
size 114689
oid sha256:9b780d9c6cdd369717d2e0981bcbcc641a5091b3835d4b7ac6315d3486098c1b
size 115915
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0d6d8a1147d9d7292a3ef66658d6bece8dca96434ae7951f4400db0a081e4337
size 334023
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:aa4dc34fbb2b7412232fb048d1ddde465f949784bc4e9af0374d7cae490a5dd3
size 211181
oid sha256:64679be3305dcfd54950f6d00ea6de2d2156a9e91c09f1c2635f222f65cd21ac
size 211035
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e293cc224e3c71f5339ca59f10db4450d96e025738c9117b18d41b57fe49a4d5
size 305117
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6656cd40298881511962f8a3fd292b990b1c029f0cf4fc8f7050e5a4fba662aa
size 305022
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a8b8dfcebe6730d4e584bf757cf0e9599130d1d9b981e77bba9a25d2cb28bc5b
size 313911
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5d436eaaeb195d07a600fd8ae093b85bf616ca6f03bffbdf17d0f7284f2ed2c1
size 319464
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3e2192501b67459b5d4f85353976e7d3a7b5cf55d5b1f712b41c736579a919be
size 303063
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ca335bfdc0d02a56eb7584e9f18004361abcb0365c30733861bbd20b7a385b3b
size 210546
oid sha256:91060605f5e1166dc33eb3481845e60ac8c49da257395b37c513bf8477b3304d
size 210405
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c7482b45bcde59ec93b1a3f211d2f3187357b5326be5a70dce4a309c8204b6a2
size 311570
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:794a4b78bd5e5cda370bfac8168d19951ef5005c5d9267d372b9b55c9accfa4b
size 373232
oid sha256:aa7c6d90c7f8021a1131a5c5db0f15aaf282548642cd97aeecac8574969771d3
size 366679
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ba8df321653a203447b6a48facc32c1b99a3167ed3f1c44019d06aa53b07db9b
size 330472
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:611e841427673aee43571e8b3079f9f84f9624b05efcc678f687ec496c9d66af
size 323568
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e17cbe7c08f8feddfeaec82d64d4c7ba001c91ef3ee5a38efd4e9eafe73e9d9b
size 335662
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:90f0175794dfebcc609105207e345e34c8d272cbd896f4bff0786ee4ecbf9c0c
size 335528
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e9b41ae6543ffcc994c2468f48641149d1932065600f94af29379d0fbb5d1297
size 283690
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:39a5f3d31a13112058a4b4c3d0b83066ff1e3a546b65e82234970b76748812ed
size 370940
oid sha256:92e93f947f47be4f87e3c997f1e9103064771a78a95836dc38b24ed92431d581
size 365103
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:180aae2e7f67beb59cfee31788efaf7f0a6b4fef910956ba1db1ed22edbfdc7f
size 114685
oid sha256:9b780d9c6cdd369717d2e0981bcbcc641a5091b3835d4b7ac6315d3486098c1b
size 115915
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0d6d8a1147d9d7292a3ef66658d6bece8dca96434ae7951f4400db0a081e4337
size 334023
+1
View File
@@ -61,6 +61,7 @@ targets:
- path: ../../ElementX/Sources/Screens/RoomScreen/View/Style/TimelineStyle.swift
- path: ../../ElementX/Sources/Services/Analytics/AnalyticsConfiguration.swift
- path: ../../ElementX/Sources/UITests/UITestScreenIdentifier.swift
- path: ../../ElementX/Sources/UITests/UITestSignalling.swift
- path: ../../ElementX/Sources/Generated/Strings.swift
- path: ../../ElementX/Sources/Generated/Strings+Untranslated.swift
- path: ../../ElementX/Resources