Add UI tests for optional app lock setup. (#2046)

This commit is contained in:
Doug
2023-11-08 15:33:40 +00:00
committed by GitHub
parent e04a55eaef
commit 07313926c9
42 changed files with 360 additions and 71 deletions
@@ -1,4 +1,4 @@
// Generated using Sourcery 2.1.1 — https://github.com/krzysztofzablocki/Sourcery
// Generated using Sourcery 2.1.2 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
// swiftlint:disable all
@@ -16,8 +16,12 @@
import Foundation
struct A11yIdentifiers {
enum A11yIdentifiers {
static let alertInfo = AlertInfo()
static let analyticsPromptScreen = AnalyticsPromptScreen()
static let appLockSetupBiometricsScreen = AppLockSetupBiometricsScreen()
static let appLockSetupPINScreen = AppLockSetupPINScreen()
static let appLockSetupSettingsScreen = AppLockSetupSettingsScreen()
static let bugReportScreen = BugReportScreen()
static let changeServerScreen = ChangeServer()
static let homeScreen = HomeScreen()
@@ -41,6 +45,25 @@ struct A11yIdentifiers {
static let notificationSettingsScreen = NotificationSettingsScreen()
static let notificationSettingsEditScreen = NotificationSettingsEditScreen()
static let createPollScreen = CreatePollScreen()
struct AlertInfo {
let primaryButton = "alert_info-primary_button"
let secondaryButton = "alert_info-secondary_button"
}
struct AppLockSetupBiometricsScreen {
let allow = "app_lock_setup_biometrics-allow"
}
struct AppLockSetupPINScreen {
let cancel = "app_lock_setup_pin-cancel"
}
struct AppLockSetupSettingsScreen {
let changePIN = "app_lock_setup_settings-change_pin"
let removePIN = "app_lock_setup_settings-remove_pin"
let removePINAlertYes = "app_lock_setup_settings-remove_pin_alert_yes"
}
struct AnalyticsPromptScreen {
let title = "analytics_prompt-title"
@@ -129,11 +129,13 @@ extension View {
Button(item.primaryButton.title, role: item.primaryButton.role) {
item.primaryButton.action?()
}
.accessibilityIdentifier(A11yIdentifiers.alertInfo.primaryButton)
if let secondaryButton = item.secondaryButton {
Button(secondaryButton.title, role: secondaryButton.role) {
secondaryButton.action?()
}
.accessibilityIdentifier(A11yIdentifiers.alertInfo.secondaryButton)
}
} message: { item in
if let message = item.message {
@@ -49,6 +49,7 @@ struct AppLockSetupBiometricsScreen: View {
.font(.system(size: 72))
.padding(.top, 58)
.padding(.bottom, 26)
.accessibilityHidden(true)
Text(context.viewState.title)
.font(.compound.headingMDBold)
@@ -66,6 +67,7 @@ struct AppLockSetupBiometricsScreen: View {
VStack(spacing: 16) {
Button(context.viewState.title) { context.send(viewAction: .allow) }
.buttonStyle(.compound(.primary))
.accessibilityIdentifier(A11yIdentifiers.appLockSetupBiometricsScreen.allow)
Button { context.send(viewAction: .skip) } label: {
Text(L10n.screenAppLockSetupBiometricUnlockSkip)
@@ -83,6 +83,7 @@ struct AppLockSetupPINScreen: View {
Button(L10n.actionCancel) {
context.send(viewAction: .cancel)
}
.accessibilityIdentifier(A11yIdentifiers.appLockSetupPINScreen.cancel)
}
}
}
@@ -25,10 +25,12 @@ struct AppLockSetupSettingsScreen: View {
Section {
ListRow(label: .plain(title: L10n.screenAppLockSettingsChangePin),
kind: .button { context.send(viewAction: .changePINCode) })
.accessibilityIdentifier(A11yIdentifiers.appLockSetupSettingsScreen.changePIN)
if !context.viewState.isMandatory {
ListRow(label: .plain(title: L10n.screenAppLockSettingsRemovePin, role: .destructive),
kind: .button { context.send(viewAction: .disable) })
.accessibilityIdentifier(A11yIdentifiers.appLockSetupSettingsScreen.removePIN)
}
}
@@ -0,0 +1,48 @@
//
// 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 LocalAuthentication
/// A customised context that allows injecting a few mock values but otherwise behaves as expected.
/// It works as the actual context does and won't update the return values of `biometryType` and
/// `evaluatedPolicyDomainStateValue` until either `canEvaluatePolicy` or
/// `evaluatePolicy` have been called.
class LAContextMock: LAContext {
var biometryTypeValue: LABiometryType!
private var internalBiometryTypeValue: LABiometryType!
override var biometryType: LABiometryType { internalBiometryTypeValue }
var evaluatedPolicyDomainStateValue: Data?
private var internalEvaluatedPolicyDomainStateValue: Data?
override var evaluatedPolicyDomainState: Data? { internalEvaluatedPolicyDomainStateValue }
override func canEvaluatePolicy(_ policy: LAPolicy, error: NSErrorPointer) -> Bool {
let result = super.canEvaluatePolicy(policy, error: error)
updateInternalValues()
return result
}
var evaluatePolicyReturnValue: Bool!
override func evaluatePolicy(_ policy: LAPolicy, localizedReason: String) async throws -> Bool {
updateInternalValues()
return evaluatePolicyReturnValue
}
private func updateInternalValues() {
internalBiometryTypeValue = biometryTypeValue
internalEvaluatedPolicyDomainStateValue = evaluatedPolicyDomainStateValue
}
}
@@ -0,0 +1,39 @@
//
// 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 SwiftUI
class BlankFormCoordinator: CoordinatorProtocol {
func toPresentable() -> AnyView {
AnyView(BlankForm())
}
}
/// An empty Form used for UI tests, behind a sheet.
private struct BlankForm: View {
var body: some View {
Form {
Text("Nothing to see here.")
}
.compoundForm()
}
}
struct BlankForm_Previews: PreviewProvider {
static var previews: some View {
BlankForm()
}
}
@@ -42,12 +42,12 @@ class UITestsAppCoordinator: AppCoordinatorProtocol {
}
func start() {
// disabling CA animations
// Fix the app tint colour.
UIApplication.shared.connectedScenes.forEach { scene in
guard let delegate = scene.delegate as? UIWindowSceneDelegate else {
return
}
delegate.window??.layer.speed = 0
delegate.window??.tintColor = .compound.textActionPrimary
}
guard let screenID = ProcessInfo.testScreenID else { fatalError("Unable to launch with unknown screen.") }
@@ -162,15 +162,36 @@ class MockScreen: Identifiable {
let appLockService = AppLockService(keychainController: KeychainControllerMock(), appSettings: ServiceLocator.shared.settings)
let coordinator = AppLockScreenCoordinator(parameters: .init(appLockService: appLockService))
return coordinator
case .appLockSetupFlow:
case .appLockSetupFlow, .appLockSetupFlowUnlock:
let navigationStackCoordinator = NavigationStackCoordinator()
let keychainController = KeychainControllerMock()
keychainController.containsPINCodeReturnValue = false
let appLockService = AppLockService(keychainController: keychainController, appSettings: ServiceLocator.shared.settings)
// The flow expects an existing root coordinator, use the placeholder as a placeholder 😅
navigationStackCoordinator.setRootCoordinator(BlankFormCoordinator())
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)
context.evaluatePolicyReturnValue = true
context.evaluatedPolicyDomainStateValue = "😎".data(using: .utf8)
let appLockService = AppLockService(keychainController: keychainController,
appSettings: ServiceLocator.shared.settings,
context: context)
let coordinator = AppLockSetupFlowCoordinator(presentingFlow: .settings,
appLockService: appLockService,
navigationStackCoordinator: navigationStackCoordinator)
coordinator.start()
retainedState.append(coordinator)
return navigationStackCoordinator
case .home:
let navigationStackCoordinator = NavigationStackCoordinator()
@@ -31,6 +31,7 @@ enum UITestsScreenIdentifier: String {
case templateScreen
case appLockScreen
case appLockSetupFlow
case appLockSetupFlowUnlock
case home
case settings
case bugReport