// // 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() init(appDelegate: AppDelegate) { windowManager = WindowManager(appDelegate: appDelegate) // disabling View animations UIView.setAnimationsEnabled(false) MXLog.configure(currentTarget: "accessibility-tests") 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 } 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): // Not sure why but some publishers seem to not properly communicate their completion, // so we added a timeout. Since we are going to migrate from publishers to streams, // this is a temporary solution var iterator = publisher .subscribe(on: DispatchQueue.main) .timeout(.seconds(1), scheduler: DispatchQueue.main) .values .makeAsyncIterator() while let value = await iterator.next(isolation: #isolation), value == false { } case .sequence(let sequence): // Mirror the publisher timeout above so a preview expecting a state that never // arrives (e.g. a transient one that already expired) doesn't hang the suite. let task = Task { var iterator = sequence.makeAsyncIterator() while let value = await iterator.next(isolation: #isolation), value == false { } } DispatchQueue.main.asyncAfter(deadline: .now() + 1) { task.cancel() } await task.value case .none: break } currentIndex = newIndex } }