mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 19:11:45 +00:00
Use files instead of UDP for signalling. (#585)
This commit is contained in:
@@ -40,6 +40,10 @@ function_body_length:
|
|||||||
warning: 50
|
warning: 50
|
||||||
error: 100
|
error: 100
|
||||||
|
|
||||||
|
nesting:
|
||||||
|
type_level:
|
||||||
|
warning: 5
|
||||||
|
|
||||||
custom_rules:
|
custom_rules:
|
||||||
print_deprecation:
|
print_deprecation:
|
||||||
regex: "\\b(print)\\b"
|
regex: "\\b(print)\\b"
|
||||||
|
|||||||
@@ -72,13 +72,22 @@
|
|||||||
"version" : "7.4.1"
|
"version" : "7.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "kzfilewatchers",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/krzysztofzablocki/KZFileWatchers",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "d27a9557427d261adccdf4b566acc9d9c0fec6f4"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "matrix-analytics-events",
|
"identity" : "matrix-analytics-events",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/matrix-org/matrix-analytics-events",
|
"location" : "https://github.com/matrix-org/matrix-analytics-events",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "main",
|
"revision" : "94598bd8ef3aa2a3d3848c656c37d5e95b6b1cff",
|
||||||
"revision" : "4bcc7f566b165cd2d8fde07d23bda77e1d9fbb2d"
|
"version" : "0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+18
-17
@@ -32,12 +32,15 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
|
|
||||||
var timelineItems: [RoomTimelineItemProtocol] = RoomTimelineItemFixtures.default
|
var timelineItems: [RoomTimelineItemProtocol] = RoomTimelineItemFixtures.default
|
||||||
|
|
||||||
private var connection: UITestsSignalling.Connection?
|
private var client: UITestsSignalling.Client?
|
||||||
|
|
||||||
init(listenForSignals: Bool = false) {
|
init(listenForSignals: Bool = false) {
|
||||||
if listenForSignals {
|
guard listenForSignals else { return }
|
||||||
connection = .init()
|
|
||||||
Task { try await startListening() }
|
do {
|
||||||
|
try startListening()
|
||||||
|
} catch {
|
||||||
|
fatalError("Failure setting up signalling: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,20 +73,18 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
|
|
||||||
// MARK: - UI Test signalling
|
// MARK: - UI Test signalling
|
||||||
|
|
||||||
|
/// The cancellable used for UI Tests signalling.
|
||||||
|
private var signalCancellable: AnyCancellable?
|
||||||
|
|
||||||
/// Allows the simulation of server responses by listening for signals from UI tests.
|
/// Allows the simulation of server responses by listening for signals from UI tests.
|
||||||
private func startListening() async throws {
|
private func startListening() throws {
|
||||||
try await connection?.connect()
|
let client = try UITestsSignalling.Client(mode: .app)
|
||||||
|
|
||||||
Task {
|
signalCancellable = client.signals.sink { [weak self] signal in
|
||||||
while let connection {
|
Task { try await self?.handleSignal(signal) }
|
||||||
do {
|
|
||||||
try await handleSignal(connection.receive())
|
|
||||||
} catch {
|
|
||||||
connection.disconnect()
|
|
||||||
self.connection = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.client = client
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles a UI test signal as necessary.
|
/// Handles a UI test signal as necessary.
|
||||||
@@ -106,7 +107,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
timelineItems.append(incomingItem)
|
timelineItems.append(incomingItem)
|
||||||
callbacks.send(.updatedTimelineItems)
|
callbacks.send(.updatedTimelineItems)
|
||||||
|
|
||||||
try? await connection?.send(.success)
|
try client?.send(.success)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prepends the next chunk of items to the `timelineItems` array.
|
/// Prepends the next chunk of items to the `timelineItems` array.
|
||||||
@@ -119,6 +120,6 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
callbacks.send(.updatedTimelineItems)
|
callbacks.send(.updatedTimelineItems)
|
||||||
callbacks.send(.isBackPaginating(false))
|
callbacks.send(.isBackPaginating(false))
|
||||||
|
|
||||||
try? await connection?.send(.success)
|
try client?.send(.success)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,13 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Network
|
import Combine
|
||||||
|
import KZFileWatchers
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
enum UITestsSignal: String {
|
enum UITestsSignal: String {
|
||||||
/// An internal signal used to bring up the connection.
|
/// An internal signal used to indicate that one side of the connection is ready.
|
||||||
case connect
|
case ready
|
||||||
/// Ask the app to back paginate.
|
/// Ask the app to back paginate.
|
||||||
case paginate
|
case paginate
|
||||||
/// Ask the app to simulate an incoming message.
|
/// Ask the app to simulate an incoming message.
|
||||||
@@ -28,211 +29,127 @@ enum UITestsSignal: String {
|
|||||||
case success
|
case success
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UITestsSignalError: Error {
|
enum UITestsSignalError: String, LocalizedError {
|
||||||
/// An unknown error occurred.
|
/// The app client failed to start as the tests client isn't ready.
|
||||||
case unknown
|
case testsClientNotReady
|
||||||
/// Signalling could not be used as is hasn't been enabled.
|
/// Failed to send a signal as a connection hasn't been established.
|
||||||
case disabled
|
|
||||||
/// The connection was cancelled.
|
|
||||||
case cancelled
|
|
||||||
/// The connection hasn't been established.
|
|
||||||
case notConnected
|
case notConnected
|
||||||
/// Attempted to receive multiple signals at once.
|
|
||||||
case awaitingAnotherSignal
|
var errorDescription: String? { "UITestsSignalError.\(rawValue)" }
|
||||||
/// Receiving the next signal timed out.
|
|
||||||
case timeout
|
|
||||||
/// A network error occurred.
|
|
||||||
case nwError(NWError)
|
|
||||||
/// An unexpected signal was received. This error isn't used internally.
|
|
||||||
case unexpected
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UITestsSignalling {
|
enum UITestsSignalling {
|
||||||
/// The Bonjour service name used for the connection. The device name
|
/// A two-way file-based signalling client that can be used to signal between the app and the UI tests runner.
|
||||||
/// is included to allow UI tests to run on multiple devices simultaneously.
|
|
||||||
private static let serviceName = "UITestsSignalling \(UIDevice.current.name) (\(Locale.current.identifier))"
|
|
||||||
/// The Bonjour service type used for the connection.
|
|
||||||
private static let serviceType = "_signalling._udp."
|
|
||||||
/// The Bonjour domain used for the connection.
|
|
||||||
private static let domain = "local."
|
|
||||||
/// The DispatchQueue used for networking.
|
|
||||||
private static let queue: DispatchQueue = .main
|
|
||||||
|
|
||||||
/// A network listener that can be used in the UI tests runner to create a two-way `Connection` with the app.
|
|
||||||
class Listener {
|
|
||||||
/// The underlying network listener.
|
|
||||||
private let listener: NWListener
|
|
||||||
|
|
||||||
/// The established connection. This is stored in case the connection is established
|
|
||||||
/// before `connection()` is awaited and so the continuation is still `nil`.
|
|
||||||
private var establishedConnection: Connection?
|
|
||||||
/// The continuation to call when a connection is established.
|
|
||||||
private var connectionContinuation: CheckedContinuation<Connection, Error>?
|
|
||||||
|
|
||||||
/// Creates a new signalling `Listener` and starts listening.
|
|
||||||
init() throws {
|
|
||||||
let service = NWListener.Service(name: UITestsSignalling.serviceName, type: UITestsSignalling.serviceType, domain: UITestsSignalling.domain)
|
|
||||||
listener = try NWListener(service: service, using: .udp)
|
|
||||||
listener.newConnectionHandler = { [weak self] nwConnection in
|
|
||||||
let connection = Connection(nwConnection: nwConnection)
|
|
||||||
nwConnection.start(queue: UITestsSignalling.queue)
|
|
||||||
nwConnection.stateUpdateHandler = { state in
|
|
||||||
switch state {
|
|
||||||
case .ready:
|
|
||||||
connection.receiveNextMessage()
|
|
||||||
self?.establishedConnection = connection
|
|
||||||
self?.connectionContinuation?.resume(returning: connection)
|
|
||||||
case .failed(let error):
|
|
||||||
self?.connectionContinuation?.resume(with: .failure(error))
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self?.listener.cancel() // Stop listening for connections when one is discovered.
|
|
||||||
}
|
|
||||||
listener.start(queue: UITestsSignalling.queue)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the negotiated `Connection` as and when it has been established.
|
|
||||||
func connection() async throws -> Connection {
|
|
||||||
guard listener.state == .setup else { throw UITestsSignalError.unknown }
|
|
||||||
if let establishedConnection {
|
|
||||||
return establishedConnection
|
|
||||||
}
|
|
||||||
return try await withCheckedThrowingContinuation { [weak self] continuation in
|
|
||||||
self?.connectionContinuation = continuation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stops the listening when a connection hasn't been established.
|
|
||||||
func cancel() {
|
|
||||||
listener.cancel()
|
|
||||||
if let connectionContinuation {
|
|
||||||
connectionContinuation.resume(throwing: UITestsSignalError.cancelled)
|
|
||||||
self.connectionContinuation = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A two-way UDP connection that can be used for signalling between the app and the UI tests runner.
|
|
||||||
/// The connection should be created as follows:
|
/// The connection should be created as follows:
|
||||||
/// - Create a `Listener` in the UI tests before launching the app. This will automatically start listening for a connection.
|
/// - Create a `Client` in `tests` mode in your UI tests before launching the app. It will start listening for signals.
|
||||||
/// - With in the App, create a `Connection` and call `connect()` to establish a connection.
|
/// - Within the app, create a `Client` in `app` mode. This will check that the tests are ready and echo back that the app is too.
|
||||||
/// - Await the `connection()` on the `Listener` when you need to send the signal.
|
/// - Call `waitForApp()` in the tests when you need to send the signal. This will suspend execution until the app has signalled it is ready.
|
||||||
/// - The two `Connection` objects can now be used for two-way signalling.
|
/// - The two `Client` objects can now be used for two-way signalling.
|
||||||
class Connection {
|
class Client {
|
||||||
/// The underlying network connection.
|
/// The file watcher responsible for receiving signals.
|
||||||
private let connection: NWConnection
|
private let fileWatcher: FileWatcher.Local
|
||||||
/// A continuation to call each time a signal is received.
|
|
||||||
private var nextMessageContinuation: CheckedContinuation<UITestsSignal, Error>?
|
|
||||||
/// A task to handle the timeout when receiving a signal.
|
|
||||||
private var nextMessageTimeoutTask: Task<Void, Never>? {
|
|
||||||
didSet {
|
|
||||||
oldValue?.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a new signalling `Connection`.
|
/// The file name used for the connection.
|
||||||
init() {
|
///
|
||||||
let endpoint = NWEndpoint.service(name: UITestsSignalling.serviceName,
|
/// The device name is included to allow UI tests to run on multiple devices simultaneously.
|
||||||
type: UITestsSignalling.serviceType,
|
/// When using parallel execution, each execution will spawn a simulator clone with its own unique name.
|
||||||
domain: UITestsSignalling.domain,
|
private let fileURL = {
|
||||||
interface: nil)
|
let directory = URL(filePath: "/Users/Shared")
|
||||||
connection = NWConnection(to: endpoint, using: .udp)
|
let deviceName = (UIDevice.current.name).replacing(" ", with: "-")
|
||||||
}
|
return directory.appending(component: "UITestsSignalling-\(deviceName)")
|
||||||
|
}()
|
||||||
|
|
||||||
/// Creates a new signalling `Connection` from an established `NWConnection`.
|
/// A mode that defines the behaviour of the client.
|
||||||
fileprivate init(nwConnection: NWConnection) {
|
enum Mode: String { case app, tests }
|
||||||
connection = nwConnection
|
/// The mode that the client is using.
|
||||||
}
|
let mode: Mode
|
||||||
|
|
||||||
/// Attempts to establish a connection with a `Listener`.
|
/// A publisher the will be sent every time a new signal is received.
|
||||||
func connect() async throws {
|
let signals = PassthroughSubject<UITestsSignal, Never>()
|
||||||
guard connection.state == .setup else { return }
|
|
||||||
|
/// Whether or not the client has established a connection.
|
||||||
|
private(set) var isConnected = false
|
||||||
|
|
||||||
|
/// Creates a new signalling `Client`.
|
||||||
|
init(mode: Mode) throws {
|
||||||
|
fileWatcher = .init(path: fileURL.path())
|
||||||
|
self.mode = mode
|
||||||
|
|
||||||
return try await withCheckedThrowingContinuation { continuation in
|
switch mode {
|
||||||
connection.start(queue: UITestsSignalling.queue)
|
case .tests:
|
||||||
connection.stateUpdateHandler = { state in
|
// The tests client is started first and writes to the file saying it is ready.
|
||||||
switch state {
|
try rawSignal(.ready).write(to: fileURL, atomically: false, encoding: .utf8)
|
||||||
case .ready:
|
case .app:
|
||||||
self.receiveNextMessage()
|
// The app client is started second and checks that there is a ready signal from the tests.
|
||||||
continuation.resume()
|
guard try String(contentsOf: fileURL) == "\(Mode.tests):\(UITestsSignal.ready)" else { throw UITestsSignalError.testsClientNotReady }
|
||||||
Task { try await self.send(.connect) }
|
isConnected = true
|
||||||
case .failed(let error):
|
// The app client then echoes back to the tests that it is now ready.
|
||||||
continuation.resume(with: .failure(error))
|
try send(.ready)
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Stops the connection.
|
|
||||||
func disconnect() {
|
|
||||||
connection.cancel()
|
|
||||||
if let nextMessageContinuation {
|
|
||||||
nextMessageContinuation.resume(throwing: UITestsSignalError.cancelled)
|
|
||||||
self.nextMessageContinuation = nil
|
|
||||||
nextMessageTimeoutTask = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends a message to the other side of the connection.
|
|
||||||
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 from the other side of the connection.
|
|
||||||
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
|
try fileWatcher.start { [weak self] result in
|
||||||
self.nextMessageContinuation = continuation
|
self?.handleFileRefresh(result)
|
||||||
|
|
||||||
// Add a 30 second timeout to stop tests from hanging
|
|
||||||
self.nextMessageTimeoutTask = Task { [weak self] in
|
|
||||||
guard let self else { return }
|
|
||||||
try? await Task.sleep(for: .seconds(30))
|
|
||||||
|
|
||||||
guard !Task.isCancelled,
|
|
||||||
let nextMessageContinuation = self.nextMessageContinuation
|
|
||||||
else { return }
|
|
||||||
|
|
||||||
nextMessageContinuation.resume(throwing: UITestsSignalError.timeout)
|
|
||||||
self.nextMessageContinuation = nil
|
|
||||||
self.nextMessageTimeoutTask = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Processes the next message received by the connection.
|
/// Suspends execution until the app's Client has signalled that it's ready.
|
||||||
fileprivate func receiveNextMessage() {
|
func waitForApp() async {
|
||||||
connection.receiveMessage { [weak self] completeContent, _, isComplete, error in
|
guard mode == .tests else { fatalError("The app can't wait for itself.") }
|
||||||
guard let self else { return }
|
|
||||||
guard isComplete else { fatalError("Partial messages not supported") }
|
guard !isConnected else { return }
|
||||||
|
await _ = signals.values.first { $0 == .ready }
|
||||||
guard let completeContent,
|
NSLog("UITestsSignalling: Connected to app.")
|
||||||
let message = String(data: completeContent, encoding: .utf8),
|
}
|
||||||
let signal = UITestsSignal(rawValue: message)
|
|
||||||
else {
|
/// Stops listening for signals.
|
||||||
let error: UITestsSignalError = error.map { .nwError($0) } ?? .unknown
|
func stop() throws {
|
||||||
self.nextMessageContinuation?.resume(with: .failure(error))
|
try fileWatcher.stop()
|
||||||
self.nextMessageContinuation = nil
|
}
|
||||||
self.nextMessageTimeoutTask = nil
|
|
||||||
return
|
/// Sends a signal.
|
||||||
}
|
func send(_ signal: UITestsSignal) throws {
|
||||||
|
guard isConnected else { throw UITestsSignalError.notConnected }
|
||||||
if signal != .connect {
|
|
||||||
self.nextMessageContinuation?.resume(returning: signal)
|
let rawSignal = rawSignal(signal)
|
||||||
self.nextMessageContinuation = nil
|
try rawSignal.write(to: fileURL, atomically: false, encoding: .utf8)
|
||||||
self.nextMessageTimeoutTask = nil
|
NSLog("UITestsSignalling: Sent \(rawSignal)")
|
||||||
}
|
}
|
||||||
|
|
||||||
self.receiveNextMessage()
|
/// The signal formatted as a string, prefixed with an identifier for the sender.
|
||||||
|
/// E.g. The tests client would produce `tests:ready` for the ready signal.
|
||||||
|
private func rawSignal(_ signal: UITestsSignal) -> String {
|
||||||
|
"\(mode.rawValue):\(signal.rawValue)"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handles a file refresh to receive a new signal.
|
||||||
|
fileprivate func handleFileRefresh(_ result: FileWatcher.RefreshResult) {
|
||||||
|
switch result {
|
||||||
|
case .noChanges:
|
||||||
|
guard let data = try? Data(contentsOf: fileURL) else { return }
|
||||||
|
processFileData(data)
|
||||||
|
case .updated(let data):
|
||||||
|
processFileData(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Processes string data from the file and publishes its signal.
|
||||||
|
private func processFileData(_ data: Data) {
|
||||||
|
guard let message = String(data: data, encoding: .utf8) else { return }
|
||||||
|
|
||||||
|
let components = message.components(separatedBy: ":")
|
||||||
|
|
||||||
|
guard components.count == 2,
|
||||||
|
components[0] != mode.rawValue, // Filter out messages sent by this client.
|
||||||
|
let signal = UITestsSignal(rawValue: components[1])
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
if signal == .ready {
|
||||||
|
isConnected = true
|
||||||
|
}
|
||||||
|
|
||||||
|
signals.send(signal)
|
||||||
|
|
||||||
|
NSLog("UITestsSignalling: Received \(message)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ targets:
|
|||||||
- package: DTCoreText
|
- package: DTCoreText
|
||||||
- package: KeychainAccess
|
- package: KeychainAccess
|
||||||
- package: Kingfisher
|
- package: Kingfisher
|
||||||
|
- package: KZFileWatchers
|
||||||
- package: PostHog
|
- package: PostHog
|
||||||
- package: SwiftyBeaver
|
- package: SwiftyBeaver
|
||||||
- package: SwiftState
|
- package: SwiftState
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ import XCTest
|
|||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class RoomScreenUITests: XCTestCase {
|
class RoomScreenUITests: XCTestCase {
|
||||||
let connectionWaitDuration: Duration = .seconds(2)
|
|
||||||
|
|
||||||
func testPlainNoAvatar() {
|
func testPlainNoAvatar() {
|
||||||
let app = Application.launch(.roomPlainNoAvatar)
|
let app = Application.launch(.roomPlainNoAvatar)
|
||||||
|
|
||||||
@@ -46,47 +44,44 @@ class RoomScreenUITests: XCTestCase {
|
|||||||
app.assertScreenshot(.roomSmallTimeline)
|
app.assertScreenshot(.roomSmallTimeline)
|
||||||
}
|
}
|
||||||
|
|
||||||
func disabled_testSmallTimelineWithIncomingAndPagination() async throws {
|
func testSmallTimelineWithIncomingAndPagination() async throws {
|
||||||
let listener = try UITestsSignalling.Listener()
|
let client = try UITestsSignalling.Client(mode: .tests)
|
||||||
|
|
||||||
let app = Application.launch(.roomSmallTimelineIncomingAndSmallPagination)
|
let app = Application.launch(.roomSmallTimelineIncomingAndSmallPagination)
|
||||||
|
|
||||||
let connection = try await listener.connection()
|
await client.waitForApp()
|
||||||
try await Task.sleep(for: connectionWaitDuration) // Allow the connection to settle on CI/Intel...
|
defer { try? client.stop() }
|
||||||
defer { connection.disconnect() }
|
|
||||||
|
|
||||||
// When a back pagination occurs and an incoming message arrives.
|
// When a back pagination occurs and an incoming message arrives.
|
||||||
try await performOperation(.incomingMessage, using: connection)
|
try await performOperation(.incomingMessage, using: client)
|
||||||
try await performOperation(.paginate, using: connection)
|
try await performOperation(.paginate, using: client)
|
||||||
|
|
||||||
// Then the 4 visible messages should stay aligned to the bottom.
|
// Then the 4 visible messages should stay aligned to the bottom.
|
||||||
app.assertScreenshot(.roomSmallTimelineIncomingAndSmallPagination)
|
app.assertScreenshot(.roomSmallTimelineIncomingAndSmallPagination)
|
||||||
}
|
}
|
||||||
|
|
||||||
func disabled_testSmallTimelineWithLargePagination() async throws {
|
func testSmallTimelineWithLargePagination() async throws {
|
||||||
let listener = try UITestsSignalling.Listener()
|
let client = try UITestsSignalling.Client(mode: .tests)
|
||||||
|
|
||||||
let app = Application.launch(.roomSmallTimelineLargePagination)
|
let app = Application.launch(.roomSmallTimelineLargePagination)
|
||||||
|
|
||||||
let connection = try await listener.connection()
|
await client.waitForApp()
|
||||||
try await Task.sleep(for: connectionWaitDuration) // Allow the connection to settle on CI/Intel...
|
defer { try? client.stop() }
|
||||||
defer { connection.disconnect() }
|
|
||||||
|
|
||||||
// When a large back pagination occurs.
|
// When a large back pagination occurs.
|
||||||
try await performOperation(.paginate, using: connection)
|
try await performOperation(.paginate, using: client)
|
||||||
|
|
||||||
// The bottom of the timeline should remain visible with more items added above.
|
// The bottom of the timeline should remain visible with more items added above.
|
||||||
app.assertScreenshot(.roomSmallTimelineLargePagination)
|
app.assertScreenshot(.roomSmallTimelineLargePagination)
|
||||||
}
|
}
|
||||||
|
|
||||||
func disabled_testTimelineLayoutInMiddle() async throws {
|
func testTimelineLayoutInMiddle() async throws {
|
||||||
let listener = try UITestsSignalling.Listener()
|
let client = try UITestsSignalling.Client(mode: .tests)
|
||||||
|
|
||||||
let app = Application.launch(.roomLayoutMiddle)
|
let app = Application.launch(.roomLayoutMiddle)
|
||||||
|
|
||||||
let connection = try await listener.connection()
|
await client.waitForApp()
|
||||||
try await Task.sleep(for: connectionWaitDuration) // Allow the connection to settle on CI/Intel...
|
defer { try? client.stop() }
|
||||||
defer { connection.disconnect() }
|
|
||||||
|
|
||||||
// Given a timeline that is neither at the top nor the bottom.
|
// Given a timeline that is neither at the top nor the bottom.
|
||||||
app.tables.element.swipeDown()
|
app.tables.element.swipeDown()
|
||||||
@@ -94,13 +89,13 @@ class RoomScreenUITests: XCTestCase {
|
|||||||
app.assertScreenshot(.roomLayoutMiddle, step: 0) // Assert initial state for comparison.
|
app.assertScreenshot(.roomLayoutMiddle, step: 0) // Assert initial state for comparison.
|
||||||
|
|
||||||
// When a back pagination occurs.
|
// When a back pagination occurs.
|
||||||
try await performOperation(.paginate, using: connection)
|
try await performOperation(.paginate, using: client)
|
||||||
|
|
||||||
// Then the UI should remain unchanged.
|
// Then the UI should remain unchanged.
|
||||||
app.assertScreenshot(.roomLayoutMiddle, step: 0)
|
app.assertScreenshot(.roomLayoutMiddle, step: 0)
|
||||||
|
|
||||||
// When an incoming message arrives
|
// When an incoming message arrives
|
||||||
try await performOperation(.incomingMessage, using: connection)
|
try await performOperation(.incomingMessage, using: client)
|
||||||
|
|
||||||
// Then the UI should still remain unchanged.
|
// Then the UI should still remain unchanged.
|
||||||
app.assertScreenshot(.roomLayoutMiddle, step: 0)
|
app.assertScreenshot(.roomLayoutMiddle, step: 0)
|
||||||
@@ -112,14 +107,13 @@ class RoomScreenUITests: XCTestCase {
|
|||||||
app.assertScreenshot(.roomLayoutMiddle, step: 1)
|
app.assertScreenshot(.roomLayoutMiddle, step: 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func disabled_testTimelineLayoutAtTop() async throws {
|
func testTimelineLayoutAtTop() async throws {
|
||||||
let listener = try UITestsSignalling.Listener()
|
let client = try UITestsSignalling.Client(mode: .tests)
|
||||||
|
|
||||||
let app = Application.launch(.roomLayoutTop)
|
let app = Application.launch(.roomLayoutTop)
|
||||||
|
|
||||||
let connection = try await listener.connection()
|
await client.waitForApp()
|
||||||
try await Task.sleep(for: connectionWaitDuration) // Allow the connection to settle on CI/Intel...
|
defer { try? client.stop() }
|
||||||
defer { connection.disconnect() }
|
|
||||||
|
|
||||||
// Given a timeline that is scrolled to the top.
|
// Given a timeline that is scrolled to the top.
|
||||||
while !app.staticTexts["Bacon ipsum dolor amet commodo incididunt ribeye dolore cupidatat short ribs."].isHittable {
|
while !app.staticTexts["Bacon ipsum dolor amet commodo incididunt ribeye dolore cupidatat short ribs."].isHittable {
|
||||||
@@ -129,23 +123,22 @@ class RoomScreenUITests: XCTestCase {
|
|||||||
app.assertScreenshot(.roomLayoutTop, insets: cropped) // Assert initial state for comparison.
|
app.assertScreenshot(.roomLayoutTop, insets: cropped) // Assert initial state for comparison.
|
||||||
|
|
||||||
// When a back pagination occurs.
|
// When a back pagination occurs.
|
||||||
try await performOperation(.paginate, using: connection)
|
try await performOperation(.paginate, using: client)
|
||||||
|
|
||||||
// Then the bottom of the timeline should remain unchanged (with new items having been added above).
|
// Then the bottom of the timeline should remain unchanged (with new items having been added above).
|
||||||
app.assertScreenshot(.roomLayoutTop, insets: cropped)
|
app.assertScreenshot(.roomLayoutTop, insets: cropped)
|
||||||
}
|
}
|
||||||
|
|
||||||
func disabled_testTimelineLayoutAtBottom() async throws {
|
func testTimelineLayoutAtBottom() async throws {
|
||||||
let listener = try UITestsSignalling.Listener()
|
let client = try UITestsSignalling.Client(mode: .tests)
|
||||||
|
|
||||||
let app = Application.launch(.roomLayoutBottom)
|
let app = Application.launch(.roomLayoutBottom)
|
||||||
|
|
||||||
let connection = try await listener.connection()
|
await client.waitForApp()
|
||||||
try await Task.sleep(for: connectionWaitDuration) // Allow the connection to settle on CI/Intel...
|
defer { try? client.stop() }
|
||||||
defer { connection.disconnect() }
|
|
||||||
|
|
||||||
// When an incoming message arrives.
|
// When an incoming message arrives.
|
||||||
try await performOperation(.incomingMessage, using: connection)
|
try await performOperation(.incomingMessage, using: client)
|
||||||
|
|
||||||
// Then the timeline should scroll down to reveal the message.
|
// Then the timeline should scroll down to reveal the message.
|
||||||
app.assertScreenshot(.roomLayoutBottom, step: 0)
|
app.assertScreenshot(.roomLayoutBottom, step: 0)
|
||||||
@@ -159,10 +152,10 @@ class RoomScreenUITests: XCTestCase {
|
|||||||
|
|
||||||
// MARK: - Helper Methods
|
// MARK: - Helper Methods
|
||||||
|
|
||||||
private func performOperation(_ operation: UITestsSignal, using connection: UITestsSignalling.Connection) async throws {
|
private func performOperation(_ operation: UITestsSignal, using client: UITestsSignalling.Client) async throws {
|
||||||
try await connection.send(operation)
|
try client.send(operation)
|
||||||
guard try await connection.receive() == .success else { throw UITestsSignalError.unexpected }
|
await _ = client.signals.values.first { $0 == .success }
|
||||||
try await Task.sleep(for: connectionWaitDuration) // Allow the timeline to update, and the connection to be ready
|
try await Task.sleep(for: .milliseconds(500)) // Allow the timeline to update
|
||||||
}
|
}
|
||||||
|
|
||||||
private func tapMessageComposer(in app: XCUIApplication) async throws {
|
private func tapMessageComposer(in app: XCUIApplication) async throws {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ targets:
|
|||||||
- package: DTCoreText
|
- package: DTCoreText
|
||||||
- package: KeychainAccess
|
- package: KeychainAccess
|
||||||
- package: Kingfisher
|
- package: Kingfisher
|
||||||
|
- package: KZFileWatchers
|
||||||
- package: PostHog
|
- package: PostHog
|
||||||
- package: SwiftyBeaver
|
- package: SwiftyBeaver
|
||||||
- package: SwiftState
|
- package: SwiftState
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Fix UI Tests for OnboardingScreen, BugReportScreen, ServerSelectionScreen, and UserSessionFlows.
|
Fix UI Tests for OnboardingScreen, BugReportScreen, ServerSelectionScreen, and UserSessionFlows. Fix UITestsSignalling by switching to file-based communication with a publisher.
|
||||||
+4
-1
@@ -46,7 +46,7 @@ packages:
|
|||||||
path: DesignKit
|
path: DesignKit
|
||||||
AnalyticsEvents:
|
AnalyticsEvents:
|
||||||
url: https://github.com/matrix-org/matrix-analytics-events
|
url: https://github.com/matrix-org/matrix-analytics-events
|
||||||
branch: main
|
exactVersion: 0.4.0
|
||||||
AppAuth:
|
AppAuth:
|
||||||
url: https://github.com/openid/AppAuth-iOS
|
url: https://github.com/openid/AppAuth-iOS
|
||||||
majorVersion: 1.5.0
|
majorVersion: 1.5.0
|
||||||
@@ -65,6 +65,9 @@ packages:
|
|||||||
Kingfisher:
|
Kingfisher:
|
||||||
url: https://github.com/onevcat/Kingfisher
|
url: https://github.com/onevcat/Kingfisher
|
||||||
majorVersion: 7.2.0
|
majorVersion: 7.2.0
|
||||||
|
KZFileWatchers:
|
||||||
|
url: https://github.com/krzysztofzablocki/KZFileWatchers
|
||||||
|
branch: master
|
||||||
Introspect:
|
Introspect:
|
||||||
url: https://github.com/siteline/SwiftUI-Introspect
|
url: https://github.com/siteline/SwiftUI-Introspect
|
||||||
majorVersion: 0.1.4
|
majorVersion: 0.1.4
|
||||||
|
|||||||
Reference in New Issue
Block a user