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 da831f6725
commit 37d88e622c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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,10 +19,13 @@ 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
@ -30,6 +33,8 @@ class UITestsAppCoordinator: AppCoordinatorProtocol {
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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.