Files
letro-ios/ElementX/Sources/AccessibilityTests/AccessibilityTestsAppCoordinator.swift
2026-03-31 20:33:33 +03:00

169 lines
6.0 KiB
Swift

//
// Copyright 2025 Element Creations Ltd.
// Copyright 2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Combine
import CoreLocation
import SwiftUI
class AccessibilityTestsAppCoordinator: AppCoordinatorProtocol {
var windowManager: any SecureWindowManagerProtocol
func handleDeepLink(_ url: URL, isExternalURL: Bool, windowType: SecondaryWindowType?) -> Bool {
fatalError("Not implemented")
}
func handleAppRoute(_ appRoute: AppRoute, windowType: SecondaryWindowType?) {
fatalError("Not implemented.")
}
func handlePotentialPhishingAttempt(url: URL, openURLAction: @escaping (URL) -> Void) -> Bool {
fatalError("Not implemented")
}
func handleUserActivity(_ userActivity: NSUserActivity) {
fatalError("Not implemented")
}
private let previewsWrapper: PreviewsWrapper
private var cancellables = Set<AnyCancellable>()
init(appDelegate: AppDelegate) {
windowManager = WindowManager(appDelegate: appDelegate)
// disabling View animations
UIView.setAnimationsEnabled(false)
MXLog.configure(currentTarget: "accessibility-tests")
ServiceLocator.shared.register(userIndicatorController: UserIndicatorController())
AppSettings.configureWithSuiteName("io.element.elementx.accessibilitytests")
AppSettings.resetAllSettings()
ServiceLocator.shared.register(appSettings: AppSettings())
let analyticsClient = AnalyticsClientMock()
analyticsClient.isRunning = false
ServiceLocator.shared.register(analytics: AnalyticsService(client: analyticsClient,
appSettings: ServiceLocator.shared.settings))
guard let name = ProcessInfo.accessibilityViewID,
let previewType = TestablePreviewsDictionary.dictionary[name] else {
fatalError("Unable to launch with unknown screen.")
}
previewsWrapper = .init(name: name, previews: previewType._allPreviews)
setupSignalling()
// Used to perform the request check before the tests run on CI, so it can be immediately dismissed.
CLLocationManager().requestWhenInUseAuthorization()
}
func toPresentable() -> AnyView {
AnyView(PreviewsWrapperView(wrapper: previewsWrapper))
}
private func setupSignalling() {
do {
let client = try UITestsSignalling.Client(mode: .app)
client.signals.sink { [weak self] signal in
guard let self else { return }
switch signal {
case .accessibilityAudit(let auditSignal):
switch auditSignal {
case .nextPreview:
Task { [weak self] in
guard let self else { return }
await previewsWrapper.updateCurrentIndex()
do {
guard !previewsWrapper.isDone else {
try client.send(.accessibilityAudit(.noMorePreviews))
return
}
try client.send(.accessibilityAudit(.nextPreviewReady(name: previewsWrapper.previewName)))
} catch {
fatalError("Failed sending signal: \(signal)")
}
}
default:
break
}
default:
break
}
}
.store(in: &cancellables)
} catch {
fatalError("Unable to start client signalling")
}
}
}
struct PreviewsWrapperView: View {
let wrapper: PreviewsWrapper
@Environment(\.dynamicTypeSize) var dynamicTypeSize
var body: some View {
if wrapper.currentIndex >= 0, !wrapper.isDone {
wrapper.currentPreview.content
.id("\(wrapper.previewName)-\(dynamicTypeSize)")
}
}
}
@Observable final class PreviewsWrapper {
private let name: String
private let previews: [_Preview]
private(set) var currentIndex = -1
var currentPreview: _Preview {
previews[currentIndex]
}
private(set) var isDone = false
var previewName: String {
"\(name)-\(currentPreview.displayName ?? String(currentIndex))"
}
init(name: String, previews: [_Preview]) {
self.name = name
self.previews = previews
}
@MainActor
func updateCurrentIndex() async {
let newIndex = currentIndex + 1
guard newIndex < previews.count else {
isDone = true
return
}
let newPreview = previews[newIndex]
var fulfillmentSource: SnapshotFulfillmentPreferenceKey.Source?
let preferenceReadingView = newPreview.content.onPreferenceChange(SnapshotFulfillmentPreferenceKey.self) { fulfillmentSource = $0?.source }
// Render an image of the view in order to trigger the preference updates to occur.
let imageRenderer = ImageRenderer(content: preferenceReadingView)
_ = imageRenderer.uiImage
switch fulfillmentSource {
case .publisher(let publisher):
_ = await publisher
// Not sure whye byt some publisher seem to not properly comunicate their completion,
// so we added a timeout. Since we are going to migrate from publishers to stream,
// this is a temporary solution
.timeout(.seconds(1), scheduler: DispatchQueue.main)
.values.first { $0 == true }
case .sequence(let sequence):
_ = await sequence.first { $0 == true }
case .none:
break
}
currentIndex = newIndex
}
}