* Add UITests for the App Lock flow. * Add Notification Signal Fix unwanted imports in UITests.
189 lines
7.4 KiB
Swift
189 lines
7.4 KiB
Swift
//
|
|
// 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 Combine
|
|
import KZFileWatchers
|
|
import SwiftUI
|
|
|
|
extension Notification.Name: Codable { }
|
|
|
|
enum UITestsSignal: Codable, Equatable {
|
|
/// An internal signal used to indicate that one side of the connection is ready.
|
|
case ready
|
|
/// The operation has completed successfully.
|
|
case success
|
|
|
|
case timeline(Timeline)
|
|
enum Timeline: Codable {
|
|
/// Ask the app to back paginate.
|
|
case paginate
|
|
/// Ask the app to simulate an incoming message.
|
|
case incomingMessage
|
|
}
|
|
|
|
/// Posts a notification.
|
|
case notification(name: Notification.Name)
|
|
}
|
|
|
|
enum UITestsSignalError: String, LocalizedError {
|
|
/// The app client failed to start as the tests client isn't ready.
|
|
case testsClientNotReady
|
|
/// Failed to send a signal as a connection hasn't been established.
|
|
case notConnected
|
|
|
|
var errorDescription: String? { "UITestsSignalError.\(rawValue)" }
|
|
}
|
|
|
|
enum UITestsSignalling {
|
|
/// A two-way file-based signalling client that can be used to signal between the app and the UI tests runner.
|
|
/// The connection should be created as follows:
|
|
/// - Create a `Client` in `tests` mode in your UI tests before launching the app. It will start listening for signals.
|
|
/// - 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.
|
|
/// - 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 `Client` objects can now be used for two-way signalling.
|
|
class Client {
|
|
/// The file watcher responsible for receiving signals.
|
|
private let fileWatcher: FileWatcher.Local
|
|
|
|
/// The file name used for the connection.
|
|
///
|
|
/// The device name is included to allow UI tests to run on multiple devices simultaneously.
|
|
/// When using parallel execution, each execution will spawn a simulator clone with its own unique name.
|
|
private let fileURL = {
|
|
let directory = URL(filePath: "/Users/Shared")
|
|
let deviceName = (UIDevice.current.name).replacing(" ", with: "-")
|
|
return directory.appending(component: "UITestsSignalling-\(deviceName)")
|
|
}()
|
|
|
|
/// A mode that defines the behaviour of the client.
|
|
enum Mode: Codable { case app, tests }
|
|
/// The mode that the client is using.
|
|
let mode: Mode
|
|
|
|
/// A publisher the will be sent every time a new signal is received.
|
|
let signals = PassthroughSubject<UITestsSignal, Never>()
|
|
|
|
/// 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
|
|
|
|
switch mode {
|
|
case .tests:
|
|
// The tests client is started first and writes to the file saying it is ready.
|
|
try rawMessage(.ready).write(to: fileURL, atomically: false, encoding: .utf8)
|
|
case .app:
|
|
// The app client is started second and checks that there is a ready signal from the tests.
|
|
guard try String(contentsOf: fileURL) == Message(mode: .tests, signal: .ready).rawValue else { throw UITestsSignalError.testsClientNotReady }
|
|
isConnected = true
|
|
// The app client then echoes back to the tests that it is now ready.
|
|
try send(.ready)
|
|
}
|
|
|
|
try fileWatcher.start { [weak self] result in
|
|
self?.handleFileRefresh(result)
|
|
}
|
|
}
|
|
|
|
/// Suspends execution until the app's Client has signalled that it's ready.
|
|
func waitForApp() async {
|
|
guard mode == .tests else { fatalError("The app can't wait for itself.") }
|
|
|
|
guard !isConnected else { return }
|
|
await _ = signals.values.first { $0 == .ready }
|
|
NSLog("UITestsSignalling: Connected to app.")
|
|
}
|
|
|
|
/// Stops listening for signals.
|
|
func stop() throws {
|
|
try fileWatcher.stop()
|
|
}
|
|
|
|
/// Sends a signal.
|
|
func send(_ signal: UITestsSignal) throws {
|
|
guard isConnected else { throw UITestsSignalError.notConnected }
|
|
|
|
let rawMessage = rawMessage(signal)
|
|
try rawMessage.write(to: fileURL, atomically: false, encoding: .utf8)
|
|
NSLog("UITestsSignalling: Sent \(rawMessage)")
|
|
}
|
|
|
|
/// The signal formatted as a complete message string, including the identifier for this sender.
|
|
private func rawMessage(_ signal: UITestsSignal) -> String {
|
|
Message(mode: mode, signal: signal).rawValue
|
|
}
|
|
|
|
/// The complete data that is serialised to disk for signalling.
|
|
/// This consists of the signal along with an identifier for the sender.
|
|
private struct Message: Codable {
|
|
let mode: Mode
|
|
let signal: UITestsSignal
|
|
|
|
init(mode: Mode, signal: UITestsSignal) {
|
|
self.mode = mode
|
|
self.signal = signal
|
|
}
|
|
|
|
var rawValue: String {
|
|
guard let data = try? JSONEncoder().encode(self),
|
|
let string = String(data: data, encoding: .utf8) else {
|
|
return "unknown"
|
|
}
|
|
return string
|
|
}
|
|
|
|
init?(rawValue: String) {
|
|
guard let data = rawValue.data(using: .utf8),
|
|
let value = try? JSONDecoder().decode(Self.self, from: data) else {
|
|
return nil
|
|
}
|
|
self = value
|
|
}
|
|
}
|
|
|
|
/// 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 rawMessage = String(data: data, encoding: .utf8) else { return }
|
|
|
|
guard let message = Message(rawValue: rawMessage),
|
|
message.mode != mode // Filter out messages sent by this client.
|
|
else { return }
|
|
|
|
if message.signal == .ready {
|
|
isConnected = true
|
|
}
|
|
|
|
signals.send(message.signal)
|
|
|
|
NSLog("UITestsSignalling: Received \(rawMessage)")
|
|
}
|
|
}
|
|
}
|