Add UI tests for AppLockFlowCoordinator. (#2055)

* Add UITests for the App Lock flow.

* Add Notification Signal

Fix unwanted imports in UITests.
This commit is contained in:
Doug
2023-11-10 15:38:54 +00:00
committed by GitHub
parent 37556463d8
commit 871726aacc
71 changed files with 348 additions and 147 deletions

View File

@@ -206,6 +206,7 @@
37906355E207DB5703754675 /* AppLockSetupBiometricsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F893F4A111CB7BA5C96949 /* AppLockSetupBiometricsScreenViewModel.swift */; };
37D789F24199B32E3FD1AA7B /* FileRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 216F0DDC98F2A2C162D09C28 /* FileRoomTimelineItemContent.swift */; };
383055C6ABE5BE058CEE1DDB /* WelcomeScreenScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57FE5EF0AFFE360C66420AAE /* WelcomeScreenScreenCoordinator.swift */; };
384D6B9A7DFD7260139D6852 /* UITestsNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBEB8D9F4940E161B18FE4BC /* UITestsNotificationCenter.swift */; };
38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */; };
386720B603F87D156DB01FB2 /* VoiceMessageMediaManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40076C770A5FB83325252973 /* VoiceMessageMediaManager.swift */; };
38896D54D6D675534E606195 /* RoomTimelineControllerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6FCC416A3BFE73DF7B3E6BF /* RoomTimelineControllerFactory.swift */; };
@@ -724,6 +725,7 @@
BA31448FBD9697F8CB9A83CD /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2245243369B99216C7D84E /* ImageCache.swift */; };
BA43D782BE85C7F5F20C624A /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; };
BA4C9049BC96DED3A2F3B82E /* RoomNotificationSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03DD998E523D4EC93C7ED703 /* RoomNotificationSettingsScreenViewModelProtocol.swift */; };
BAC845780F17CCFBC5A9CA37 /* AppLockUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F733F135E6D67BBBEB76CC30 /* AppLockUITests.swift */; };
BB6BF528BC7F5B87E08C4F18 /* CameraPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A3B7637DDBD6AA97AC2545 /* CameraPicker.swift */; };
BB784A02BADB03C820617A46 /* TextRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90A55430639712CFACA34F43 /* TextRoomTimelineItem.swift */; };
BB9B800C6094E34860E89DC5 /* AppLockSetupBiometricsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CCF9A924521DECA44778C4 /* AppLockSetupBiometricsScreen.swift */; };
@@ -918,7 +920,6 @@
EF5009AC03212227131C8AF2 /* RoomNotificationSettingsProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E55B5EA766E89FF1F87C3ACB /* RoomNotificationSettingsProxyProtocol.swift */; };
EF7924005216B8189898F370 /* BackgroundTaskProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA028DCD4157F9A1F999827 /* BackgroundTaskProtocol.swift */; };
EF890DEF0479E66548F2BA23 /* AppLockTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490BEADEFB2D6B7C9F618AE8 /* AppLockTimer.swift */; };
F05516474DB42369FD976CEF /* AppLockScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349C633291427A0F29C28C54 /* AppLockScreenUITests.swift */; };
F0570F1ECD70C4C851FB2052 /* SecureBackupRecoveryKeyScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E7304F5ECB4CB11CB10E60 /* SecureBackupRecoveryKeyScreenViewModelProtocol.swift */; };
F06CE9132855E81EBB6DDC32 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 940C605265DD82DA0C655E23 /* Kingfisher */; };
F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */; };
@@ -1215,7 +1216,6 @@
33E49C5C6F802B4D94CA78D1 /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ro; path = ro.lproj/Localizable.strings; sourceTree = "<group>"; };
340179A0FC1AD4AEDA7FC134 /* CreateRoomViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomViewModelProtocol.swift; sourceTree = "<group>"; };
342BEBC3C5FC3F9943C41C4C /* TemplateScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModelProtocol.swift; sourceTree = "<group>"; };
349C633291427A0F29C28C54 /* AppLockScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenUITests.swift; sourceTree = "<group>"; };
351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReactionsView.swift; sourceTree = "<group>"; };
3558A15CFB934F9229301527 /* RestorationToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorationToken.swift; sourceTree = "<group>"; };
35AFCF4C05DEED04E3DB1A16 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
@@ -1855,6 +1855,7 @@
E9D059BFE329BE09B6D96A9F /* ro */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ro; path = ro.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
EB3B237387B8288A5A938F1B /* UserAgentBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentBuilderTests.swift; sourceTree = "<group>"; };
EBBC5E7C0F8337D2A46EB2DD /* MigrationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationScreenViewModelProtocol.swift; sourceTree = "<group>"; };
EBEB8D9F4940E161B18FE4BC /* UITestsNotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsNotificationCenter.swift; sourceTree = "<group>"; };
EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsViewModelTests.swift; sourceTree = "<group>"; };
ECB08484CD5D77C9BF97AA78 /* WaitlistScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenUITests.swift; sourceTree = "<group>"; };
ECD5FCBA169B6A82F501CA1B /* AnalyticsSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
@@ -1892,6 +1893,7 @@
F5E23D8EE6CBACF32F1EC874 /* MediaPlayerProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerProviderProtocol.swift; sourceTree = "<group>"; };
F6D698BFD68B061350553930 /* WaitingDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitingDialog.swift; sourceTree = "<group>"; };
F72EFC8C634469F9262659C7 /* NSItemProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSItemProvider.swift; sourceTree = "<group>"; };
F733F135E6D67BBBEB76CC30 /* AppLockUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockUITests.swift; sourceTree = "<group>"; };
F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattedBodyText.swift; sourceTree = "<group>"; };
F7478623CECC9438014244BA /* ServerConfirmationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreen.swift; sourceTree = "<group>"; };
F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermalinkBuilder.swift; sourceTree = "<group>"; };
@@ -2400,6 +2402,7 @@
children = (
8F94F70480243CAA65A2008C /* BlanckFormCoordinator.swift */,
46C208DA43CE25D13E670F40 /* UITestsAppCoordinator.swift */,
EBEB8D9F4940E161B18FE4BC /* UITestsNotificationCenter.swift */,
6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */,
B7F0192CE2F891141A25B49F /* UITestsSignalling.swift */,
);
@@ -3694,8 +3697,8 @@
AF11DD57D9FACF2A757AB024 /* AnalyticsPromptUITests.swift */,
16037EE9E9A52AF37B7818E3 /* AnalyticsSettingsScreenUITests.swift */,
7D0CBC76C80E04345E11F2DB /* Application.swift */,
349C633291427A0F29C28C54 /* AppLockScreenUITests.swift */,
E8A1BBEF7318CA6B6ACCF4AE /* AppLockSetupUITests.swift */,
F733F135E6D67BBBEB76CC30 /* AppLockUITests.swift */,
5D2D0A6F1ABC99D29462FB84 /* AuthenticationCoordinatorUITests.swift */,
C6FEA87EA3752203065ECE27 /* BugReportUITests.swift */,
1D8866FE1CCCF10305FCACBC /* CallScreenUITests.swift */,
@@ -5854,6 +5857,7 @@
706F79A39BDB32F592B8C2C7 /* UIKitBackgroundTask.swift in Sources */,
3097A0A867D2B19CE32DAE58 /* UIKitBackgroundTaskService.swift in Sources */,
E96005321849DBD7C72A28F2 /* UITestsAppCoordinator.swift in Sources */,
384D6B9A7DFD7260139D6852 /* UITestsNotificationCenter.swift in Sources */,
22882C710BC99EC34A5024A0 /* UITestsScreenIdentifier.swift in Sources */,
706289B086B0A6B0C211763F /* UITestsSignalling.swift in Sources */,
245F7FE5961BD10C145A26E0 /* UITimelineView.swift in Sources */,
@@ -5936,8 +5940,8 @@
795A854F63301DC6B46217B9 /* AccessibilityIdentifiers.swift in Sources */,
8024BE37156FF0A95A7A3465 /* AnalyticsPromptUITests.swift in Sources */,
BF675964C9159F718589C36A /* AnalyticsSettingsScreenUITests.swift in Sources */,
F05516474DB42369FD976CEF /* AppLockScreenUITests.swift in Sources */,
44DA28B1E1F9C97C5795F7B3 /* AppLockSetupUITests.swift in Sources */,
BAC845780F17CCFBC5A9CA37 /* AppLockUITests.swift in Sources */,
7405B4824D45BA7C3D943E76 /* Application.swift in Sources */,
ACF094CF3BF02DBFA6DFDE60 /* AuthenticationCoordinatorUITests.swift in Sources */,
7756C4E90CABE6F14F7920A0 /* BugReportUITests.swift in Sources */,

View File

@@ -18,5 +18,6 @@ import Foundation
protocol AppCoordinatorProtocol: CoordinatorProtocol {
var notificationManager: NotificationManagerProtocol { get }
var windowManager: WindowManager { get }
@discardableResult func handleDeepLink(_ url: URL) -> Bool
}

View File

@@ -15,8 +15,7 @@
//
import Combine
import Foundation
import UIKit
import SwiftUI
enum AppDelegateCallback {
case registeredNotifications(deviceToken: Data)

View File

@@ -29,10 +29,9 @@ struct Application: App {
} else if ProcessInfo.isRunningUnitTests {
appCoordinator = UnitTestsAppCoordinator()
} else {
let coordinator = AppCoordinator(appDelegate: appDelegate)
SceneDelegate.windowManager = coordinator.windowManager
appCoordinator = coordinator
appCoordinator = AppCoordinator(appDelegate: appDelegate)
}
SceneDelegate.windowManager = appCoordinator.windowManager
}
var body: some Scene {

View File

@@ -23,7 +23,7 @@ class SceneDelegate: NSObject, UIWindowSceneDelegate {
weak static var windowManager: WindowManager!
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene, !ProcessInfo.isRunningTests else { return }
guard let windowScene = scene as? UIWindowScene else { return }
Self.windowManager.configure(with: windowScene)
}
}

View File

@@ -40,7 +40,9 @@ class AppLockFlowCoordinator: CoordinatorProtocol {
actionsSubject.eraseToAnyPublisher()
}
init(appLockService: AppLockServiceProtocol, navigationCoordinator: NavigationRootCoordinator) {
init(appLockService: AppLockServiceProtocol,
navigationCoordinator: NavigationRootCoordinator,
notificationCenter: NotificationCenter = .default) {
self.appLockService = appLockService
self.navigationCoordinator = navigationCoordinator
@@ -54,13 +56,13 @@ class AppLockFlowCoordinator: CoordinatorProtocol {
}
.store(in: &cancellables)
NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)
notificationCenter.publisher(for: UIApplication.didEnterBackgroundNotification)
.sink { [weak self] _ in
self?.applicationDidEnterBackground()
}
.store(in: &cancellables)
NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
notificationCenter.publisher(for: UIApplication.willEnterForegroundNotification)
.sink { [weak self] _ in
self?.applicationWillEnterForeground()
}

View File

@@ -19,6 +19,7 @@ import Foundation
enum A11yIdentifiers {
static let alertInfo = AlertInfo()
static let analyticsPromptScreen = AnalyticsPromptScreen()
static let appLockScreen = AppLockScreen()
static let appLockSetupBiometricsScreen = AppLockSetupBiometricsScreen()
static let appLockSetupPINScreen = AppLockSetupPINScreen()
static let appLockSetupSettingsScreen = AppLockSetupSettingsScreen()
@@ -51,6 +52,10 @@ enum A11yIdentifiers {
let secondaryButton = "alert_info-secondary_button"
}
struct AppLockScreen {
func numpad(_ digit: Int) -> String { "app_lock-numpad_\(digit)" }
}
struct AppLockSetupBiometricsScreen {
let allow = "app_lock_setup_biometrics-allow"
}

View File

@@ -27,12 +27,14 @@ struct AppLockScreenPINKeypad: View {
ForEach(1..<4) { column in
let digit = (3 * row) + column
Button("\(digit)") { press(digit) }
.accessibilityIdentifier(A11yIdentifiers.appLockScreen.numpad(digit))
}
}
}
GridRow {
Button("") { }.hidden()
Button("0") { press(0) }
.accessibilityIdentifier(A11yIdentifiers.appLockScreen.numpad(0))
Button(action: pressDelete) {
Image(systemSymbol: .deleteBackward)
.symbolVariant(.fill)

View File

@@ -125,9 +125,9 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
/// Handles a UI test signal as necessary.
private func handleSignal(_ signal: UITestsSignal) async throws {
switch signal {
case .paginate:
case .timeline(.paginate):
try await simulateBackPagination()
case .incomingMessage:
case .timeline(.incomingMessage):
try await simulateIncomingItem()
default:
break

View File

@@ -19,17 +19,22 @@ import MatrixRustSDK
import SwiftUI
import UIKit
class UITestsAppCoordinator: AppCoordinatorProtocol {
class UITestsAppCoordinator: AppCoordinatorProtocol, WindowManagerDelegate {
private let navigationRootCoordinator: NavigationRootCoordinator
private var mockScreen: MockScreen?
private var alternateWindowMockScreen: MockScreen?
let notificationManager: NotificationManagerProtocol = NotificationManagerMock()
let windowManager = WindowManager()
init() {
// disabling View animations
UIView.setAnimationsEnabled(false)
navigationRootCoordinator = NavigationRootCoordinator()
windowManager.delegate = self
ServiceLocator.shared.register(userIndicatorController: UserIndicatorControllerMock.default)
AppSettings.configureWithSuiteName("io.element.elementx.uitests")
@@ -42,14 +47,6 @@ class UITestsAppCoordinator: AppCoordinatorProtocol {
}
func start() {
// Fix the app tint colour.
UIApplication.shared.connectedScenes.forEach { scene in
guard let delegate = scene.delegate as? UIWindowSceneDelegate else {
return
}
delegate.window??.tintColor = .compound.textActionPrimary
}
guard let screenID = ProcessInfo.testScreenID else { fatalError("Unable to launch with unknown screen.") }
let mockScreen = MockScreen(id: screenID)
@@ -64,17 +61,27 @@ class UITestsAppCoordinator: AppCoordinatorProtocol {
func handleDeepLink(_ url: URL) -> Bool {
fatalError("Not implemented.")
}
func windowManagerDidConfigureWindows(_ windowManager: WindowManager) {
guard let screenID = ProcessInfo.testScreenID, screenID == .appLockFlow || screenID == .appLockFlowDisabled else { return }
let screen = MockScreen(id: screenID == .appLockFlow ? .appLockFlowAlternateWindow : .appLockFlowDisabledAlternateWindow, windowManager: windowManager)
windowManager.alternateWindow.rootViewController = UIHostingController(rootView: screen.coordinator.toPresentable().statusBarHidden())
alternateWindowMockScreen = screen
}
}
@MainActor
class MockScreen: Identifiable {
let id: UITestsScreenIdentifier
let windowManager: WindowManager?
private var retainedState = [Any]()
private var cancellables = Set<AnyCancellable>()
init(id: UITestsScreenIdentifier) {
init(id: UITestsScreenIdentifier, windowManager: WindowManager? = nil) {
self.id = id
self.windowManager = windowManager
}
lazy var coordinator: CoordinatorProtocol = {
@@ -158,24 +165,15 @@ class MockScreen: Identifiable {
let coordinator = TemplateScreenCoordinator(parameters: .init())
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .appLockScreen:
let appLockService = AppLockService(keychainController: KeychainControllerMock(), appSettings: ServiceLocator.shared.settings)
let coordinator = AppLockScreenCoordinator(parameters: .init(appLockService: appLockService))
return coordinator
case .appLockSetupFlow, .appLockSetupFlowUnlock, .appLockSetupFlowMandatory:
let navigationStackCoordinator = NavigationStackCoordinator()
// The flow expects an existing root coordinator, use the placeholder as a placeholder 😅
navigationStackCoordinator.setRootCoordinator(BlankFormCoordinator())
case .appLockFlow, .appLockFlowDisabled:
// The tested coordinator is setup below in the alternate window.
// Here we just return a blank screen to snapshot as the unlocked app.
return BlankFormCoordinator()
case .appLockFlowAlternateWindow, .appLockFlowDisabledAlternateWindow:
let navigationCoordinator = NavigationRootCoordinator()
let keychainController = KeychainController(service: .tests, accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
keychainController.resetSecrets()
if id == .appLockSetupFlowUnlock {
do {
try keychainController.setPINCode("2023")
} catch {
fatalError("Failed to pre-set the PIN code")
}
}
let context = LAContextMock()
context.biometryTypeValue = UIDevice.current.isPhone ? .faceID : .touchID // (iPhone 14 & iPad 9th gen)
@@ -186,6 +184,59 @@ class MockScreen: Identifiable {
appSettings: ServiceLocator.shared.settings,
context: context)
if id == .appLockFlowAlternateWindow {
guard case .success = appLockService.setupPINCode("2023") else {
fatalError("Failed to preset the PIN code.")
}
}
let notificationCenter = UITestsNotificationCenter()
do {
try notificationCenter.startListening()
} catch {
fatalError("Failed to start listening for notifications.")
}
let coordinator = AppLockFlowCoordinator(appLockService: appLockService,
navigationCoordinator: navigationCoordinator,
notificationCenter: notificationCenter)
guard let windowManager else { fatalError("The window manager must be supplied.") }
coordinator.actions
.sink { action in
switch action {
case .lockApp:
windowManager.switchToAlternate()
case .unlockApp:
windowManager.switchToMain()
case .forceLogout:
break
}
}
.store(in: &cancellables)
return coordinator
case .appLockSetupFlow, .appLockSetupFlowUnlock, .appLockSetupFlowMandatory:
let navigationStackCoordinator = NavigationStackCoordinator()
// The flow expects an existing root coordinator, use a blank form as a placeholder.
navigationStackCoordinator.setRootCoordinator(BlankFormCoordinator())
let keychainController = KeychainController(service: .tests, accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
keychainController.resetSecrets()
let context = LAContextMock()
context.biometryTypeValue = UIDevice.current.isPhone ? .faceID : .touchID // (iPhone 14 & iPad 9th gen)
context.evaluatePolicyReturnValue = true
context.evaluatedPolicyDomainStateValue = "😎".data(using: .utf8)
let appLockService = AppLockService(keychainController: keychainController,
appSettings: ServiceLocator.shared.settings,
context: context)
if id == .appLockSetupFlowUnlock, case .failure = appLockService.setupPINCode("2023") {
fatalError("Failed to pre-set the PIN code")
}
let flow: AppLockSetupFlowCoordinator.PresentationFlow = id == .appLockSetupFlowMandatory ? .onboarding : .settings
let coordinator = AppLockSetupFlowCoordinator(presentingFlow: flow,
appLockService: appLockService,

View File

@@ -0,0 +1,57 @@
//
// Copyright 2023 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 SwiftUI
@MainActor
/// A notification center that can be injected in the app to post notifications
/// that are sent from the UI tests runner. Usage:
/// - Create an instance of the center in the screen you want to test and call `startListening`.
/// - Create a `UITestSignalling.Client` in the `.tests` mode in your tests.
/// - Start the app from the tests and call `client.waitForApp()` to establish communication.
/// - Send the notification from the tests you would like posted in the app.
class UITestsNotificationCenter: NotificationCenter {
private var client: UITestsSignalling.Client?
private var signalCancellable: AnyCancellable?
/// Starts listening for signals to post notifications.
func startListening() throws {
let client = try UITestsSignalling.Client(mode: .app)
signalCancellable = client.signals.sink { [weak self] signal in
Task {
do {
try await self?.handleSignal(signal)
} catch {
MXLog.error(error.localizedDescription)
}
}
}
self.client = client
}
/// Handles any notification signals, and drops anything else received.
private func handleSignal(_ signal: UITestsSignal) async throws {
switch signal {
case .notification(let name):
post(name: name, object: nil)
default:
break
}
}
}

View File

@@ -29,7 +29,10 @@ enum UITestsScreenIdentifier: String {
case analyticsSettingsScreen
case migration
case templateScreen
case appLockScreen
case appLockFlow
case appLockFlowAlternateWindow
case appLockFlowDisabled
case appLockFlowDisabledAlternateWindow
case appLockSetupFlow
case appLockSetupFlowUnlock
case appLockSetupFlowMandatory

View File

@@ -18,15 +18,24 @@ import Combine
import KZFileWatchers
import SwiftUI
enum UITestsSignal: String {
extension Notification.Name: Codable { }
enum UITestsSignal: Codable, Equatable {
/// An internal signal used to indicate that one side of the connection is ready.
case ready
/// 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
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 {
@@ -60,7 +69,7 @@ enum UITestsSignalling {
}()
/// A mode that defines the behaviour of the client.
enum Mode: String { case app, tests }
enum Mode: Codable { case app, tests }
/// The mode that the client is using.
let mode: Mode
@@ -78,10 +87,10 @@ enum UITestsSignalling {
switch mode {
case .tests:
// The tests client is started first and writes to the file saying it is ready.
try rawSignal(.ready).write(to: fileURL, atomically: false, encoding: .utf8)
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) == "\(Mode.tests):\(UITestsSignal.ready)" else { throw UITestsSignalError.testsClientNotReady }
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)
@@ -110,15 +119,42 @@ enum UITestsSignalling {
func send(_ signal: UITestsSignal) throws {
guard isConnected else { throw UITestsSignalError.notConnected }
let rawSignal = rawSignal(signal)
try rawSignal.write(to: fileURL, atomically: false, encoding: .utf8)
NSLog("UITestsSignalling: Sent \(rawSignal)")
let rawMessage = rawMessage(signal)
try rawMessage.write(to: fileURL, atomically: false, encoding: .utf8)
NSLog("UITestsSignalling: Sent \(rawMessage)")
}
/// 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)"
/// 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.
@@ -134,22 +170,19 @@ enum UITestsSignalling {
/// 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 }
guard let rawMessage = 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])
guard let message = Message(rawValue: rawMessage),
message.mode != mode // Filter out messages sent by this client.
else { return }
if signal == .ready {
if message.signal == .ready {
isConnected = true
}
signals.send(signal)
signals.send(message.signal)
NSLog("UITestsSignalling: Received \(message)")
NSLog("UITestsSignalling: Received \(rawMessage)")
}
}
}

View File

@@ -18,6 +18,7 @@ import SwiftUI
class UnitTestsAppCoordinator: AppCoordinatorProtocol {
let notificationManager: NotificationManagerProtocol = NotificationManagerMock()
let windowManager = WindowManager()
init() {
ServiceLocator.shared.register(userIndicatorController: UserIndicatorControllerMock.default)

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -1,26 +0,0 @@
//
// 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 ElementX
import XCTest
@MainActor
class AppLockScreenUITests: XCTestCase {
func testScreen() async throws {
let app = Application.launch(.appLockScreen)
try await app.assertScreenshot(.appLockScreen)
}
}

View File

@@ -16,8 +16,6 @@
import XCTest
@testable import ElementX
@MainActor
class AppLockSetupUITests: XCTestCase {
var app: XCUIApplication!

View File

@@ -0,0 +1,87 @@
//
// 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 XCTest
@MainActor
class AppLockUITests: XCTestCase {
var app: XCUIApplication!
enum Step {
static let placeholder = 0
static let lockScreen = 1
static let unlocked = 99
}
func testFlowEnabled() async throws {
// Given an app with screen lock enabled.
let client = try UITestsSignalling.Client(mode: .tests)
app = Application.launch(.appLockFlow)
await client.waitForApp()
// Blank form representing an unlocked app.
try await app.assertScreenshot(.appLockFlow, step: Step.unlocked)
// When backgrounding the app.
try client.send(.notification(name: UIApplication.didEnterBackgroundNotification))
// Then the placeholder screen should obscure the content.
try await app.assertScreenshot(.appLockFlow, step: Step.placeholder)
// When foregrounding the app.
try client.send(.notification(name: UIApplication.willEnterForegroundNotification))
// Then the Lock Screen should be shown to enter a PIN.
try await app.assertScreenshot(.appLockFlow, step: Step.lockScreen)
// When entering a PIN
enterPIN()
// Then the app should be unlocked again.
try await app.assertScreenshot(.appLockFlow, step: Step.unlocked)
}
func testFlowDisabled() async throws {
// Given an app with screen lock enabled.
let client = try UITestsSignalling.Client(mode: .tests)
app = Application.launch(.appLockFlowDisabled)
await client.waitForApp()
// Blank form representing an unlocked app.
try await app.assertScreenshot(.appLockFlow, step: Step.unlocked)
// When backgrounding the app.
try client.send(.notification(name: UIApplication.didEnterBackgroundNotification))
// Then the app should remain unlocked.
try await app.assertScreenshot(.appLockFlow, step: Step.unlocked)
// When foregrounding the app.
try client.send(.notification(name: UIApplication.willEnterForegroundNotification))
// Then the app should still remain unlocked.
try await app.assertScreenshot(.appLockFlow, step: Step.unlocked)
}
// MARK: - Helpers
func enterPIN() {
app.buttons[A11yIdentifiers.appLockScreen.numpad(2)].tap()
app.buttons[A11yIdentifiers.appLockScreen.numpad(0)].tap()
app.buttons[A11yIdentifiers.appLockScreen.numpad(2)].tap()
app.buttons[A11yIdentifiers.appLockScreen.numpad(3)].tap()
}
}

View File

@@ -16,8 +16,6 @@
import XCTest
@testable import ElementX
@MainActor
class AuthenticationCoordinatorUITests: XCTestCase {
func testLoginWithPassword() async throws {

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor
@@ -181,8 +180,8 @@ class RoomScreenUITests: XCTestCase {
// MARK: - Helper Methods
private func performOperation(_ operation: UITestsSignal, using client: UITestsSignalling.Client) async throws {
try client.send(operation)
private func performOperation(_ operation: UITestsSignal.Timeline, using client: UITestsSignalling.Client) async throws {
try client.send(.timeline(operation))
await _ = client.signals.values.first { $0 == .success }
try await Task.sleep(for: .seconds(2)) // Allow the timeline to update
}

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -14,7 +14,6 @@
// limitations under the License.
//
import ElementX
import XCTest
@MainActor

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:19be252acb1287d614ee24423930c02e9470aa0c1a39a58a56573098d3de5e4c
size 1296179

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5ea38cf925c1ecb6f681f70f9e69261e04458f315037c826608c62827b4d4a43
size 100758

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7d09d2089cd5d540e12aaa823da60e20806c25202f0f3b60bfd4cebc8978e8a9
size 58054

View File

@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:02d0e19e329769fd507b7e02cbb95c55f36db403ba221b1362fea51e8e37a026
size 100756

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5d2509f4aede5aab31616f52227ae55547e62060f59fe57498cefaaa65a7fe00
size 937763

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:218cd540d96ff548dfcd300e3d6819cec8675756b1581d3bff9ad65a5dec8222
size 119101

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:af5d5eeb0ec353137c190a149bdb394da32faa730f17f3667768992cb9db0ee9
size 57774

View File

@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:326cb365efab4ca083b39104402e1056ae915e4a230dacd07f494343e427cef7
size 117772

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:19be252acb1287d614ee24423930c02e9470aa0c1a39a58a56573098d3de5e4c
size 1296179

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ade0d3ff831c271820d475203689b69ca1b935bc32d8c6ad1c3e31a1ba3d339b
size 108963

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e4f8dfd9c3e7bf9c5b7fe6ccd94c7a710b331ee9920a658c664287fa4d6403f7
size 58873

View File

@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c5b462d8c9213706f06d4dab74a2b51bd29a5b961748ad3fd8156e48d407d832
size 108960

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fddcd92de45f0d1c41bb3c4a1555a48b91a0f9c23582ef780aa38fdec0f08186
size 936389

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f287ea8b97c94e38ddddb0cf3ef81d25eecc2b0136a4b68d0ac48779e13ab1ed
size 127569

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b6f97fa428f0cccd0b72f2033c3b7c54c5f15f80a7293fb1b58da88704cf91b7
size 60147

View File

@@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2d439b5c3e450115d6e46ae632ab3486a54763478db5e2111a763b9ab71da0cb
size 127567