Various UI test fixes (#370)

* Increase integration tests time limits again as they're still ocasionally failing

* Fixed NavigationRootCoordinator name in logs

* Refactor UI tests hierarchy and introduce new userFlowScreen

* Introduce a RoomTimelineControllerFactory so that it can be mocked in the UserFlow UI tests

* Start using a mock timeline controller for the UserSession flows

* Remove the WeakDictionary dependency and replce it with a plain NSMapTable in the BackgroundTaskService

* Allow multiple UITests screenshots per screen

* Prevent the view hierarchy changing when taking screenshots

* Add UserSessionScreen UI tests

* Fix label triaging workflow project identifier as per vector-im/element-ios/pull/7150

* Fix settings screen tests

* Fix roomPlainNoAvatar and roomEncryptedWithAvatar UI tests

* Fix modal server selection screen UI tests

* Fix bug report and login screen UI tests

* Fix text typing missing characters on UI tests

* Fix sliding sync configuration on integration tests

* Stop crashing if not finding a particular room through the MockClientProxy
This commit is contained in:
Stefan Ceriu
2022-12-15 15:22:39 +02:00
committed by GitHub
parent cfd563786d
commit 5b90f37f2e
66 changed files with 454 additions and 583 deletions
@@ -219,7 +219,8 @@ class AppCoordinator: AppCoordinatorProtocol {
let navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: SplashScreenCoordinator())
let userSessionFlowCoordinator = UserSessionFlowCoordinator(userSession: userSession,
navigationSplitCoordinator: navigationSplitCoordinator,
bugReportService: bugReportService)
bugReportService: bugReportService,
roomTimelineControllerFactory: RoomTimelineControllerFactory())
userSessionFlowCoordinator.callback = { [weak self] action in
switch action {
@@ -57,9 +57,9 @@ class NavigationRootCoordinator: ObservableObject, CoordinatorProtocol, CustomSt
var description: String {
if let rootCoordinator = rootModule?.coordinator {
return "SingleScreenCoordinator(\(rootCoordinator)"
return "NavigationRootCoordinator(\(rootCoordinator)"
} else {
return "SingleScreenCoordinator(Empty)"
return "NavigationRootCoordinator(Empty)"
}
}
@@ -0,0 +1,62 @@
//
// 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
extension XCUIElement {
func clearAndTypeText(_ text: String) {
let maxAttemptCount = 10
var attemptCount = 0
repeat {
tap()
guard let currentValue = value as? String else {
XCTFail("Tried to clear and type text into a non string value")
return
}
let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: currentValue.count)
typeText(deleteString)
typeText(text)
if !exists { // Break if the element in question doesn't exist anymore
break
}
guard let newValue = value as? String else {
XCTFail("Tried to clear and type text into a non string value")
return
}
if newValue == String(repeating: "•", count: text.count) { // Secure entry text field
break
}
if newValue == text.trimmingCharacters(in: .whitespacesAndNewlines) {
break
}
attemptCount += 1
if attemptCount > maxAttemptCount {
XCTFail("Failed clearAndTypeText after \(maxAttemptCount) attempts.")
return
}
} while true
}
}
@@ -83,6 +83,7 @@ struct HomeScreenRoomCell: View {
context.send(viewAction: .loadRoomData(roomIdentifier: room.id))
}
}
.accessibilityIdentifier("roomName:\(room.name)")
}
var lastMessageFont: Font {
@@ -20,7 +20,7 @@ import UIKit
/// /// UIKitBackgroundTaskService is a concrete implementation of BackgroundTaskServiceProtocol using a given `ApplicationProtocol` instance.
class UIKitBackgroundTaskService: BackgroundTaskServiceProtocol {
private let applicationBlock: () -> ApplicationProtocol?
private var reusableTasks: WeakDictionary<String, UIKitBackgroundTask> = WeakDictionary()
private var reusableTasks = NSMapTable<NSString, UIKitBackgroundTask>(keyOptions: .strongMemory, valueOptions: .weakMemory)
private var application: ApplicationProtocol? {
applicationBlock()
@@ -50,7 +50,7 @@ class UIKitBackgroundTaskService: BackgroundTaskServiceProtocol {
var result: BackgroundTaskProtocol?
if isReusable {
if let oldTask = reusableTasks[name], oldTask.isRunning {
if let oldTask = reusableTasks.object(forKey: name as NSString), oldTask.isRunning {
oldTask.reuse()
result = oldTask
} else {
@@ -59,11 +59,11 @@ class UIKitBackgroundTaskService: BackgroundTaskServiceProtocol {
application: application,
expirationHandler: { [weak self] task in
guard let self else { return }
self.reusableTasks[task.name] = nil
self.reusableTasks.removeObject(forKey: task.name as NSString)
expirationHandler?()
}) {
created = true
reusableTasks[name] = newTask
reusableTasks.setObject(newTask, forKey: name as NSString)
result = newTask
}
}
@@ -41,7 +41,12 @@ class MockClientProxy: ClientProxyProtocol {
func restartSync() { }
func roomForIdentifier(_ identifier: String) async -> RoomProxyProtocol? {
nil
guard let room = roomSummaryProvider?.roomListPublisher.value.first(where: { $0.id == identifier }),
let displayName = room.asFilled?.name else {
return nil
}
return MockRoomProxy(displayName: displayName)
}
func loadUserDisplayName() async -> Result<String, ClientProxyError> {
@@ -55,7 +55,7 @@ class MockRoomSummaryProvider: RoomSummaryProviderProtocol {
isDirect: true,
avatarURLString: nil,
lastMessage: AttributedString("Prosciutto beef ribs pancetta filet mignon kevin hamburger, chuck ham venison picanha. Beef ribs chislic turkey biltong tenderloin."),
lastMessageTimestamp: .now,
lastMessageTimestamp: .distantPast,
unreadNotificationCount: 4)),
.filled(details: RoomSummaryDetails(id: "2",
name: "Second room",
@@ -69,7 +69,7 @@ class MockRoomSummaryProvider: RoomSummaryProviderProtocol {
isDirect: true,
avatarURLString: nil,
lastMessage: try? AttributedString(markdown: "**@mock:client.com**: T-bone beef ribs bacon"),
lastMessageTimestamp: .now,
lastMessageTimestamp: .distantPast,
unreadNotificationCount: 0)),
.empty(id: "3")
]
@@ -0,0 +1,28 @@
//
// 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 Foundation
struct MockRoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol {
func buildRoomTimelineController(userId: String,
roomProxy: RoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) -> RoomTimelineControllerProtocol {
let timelineController = MockRoomTimelineController()
timelineController.timelineItems = RoomTimelineItemFixtures.largeChunk
return timelineController
}
}
@@ -32,19 +32,20 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
}
let roomId: String
let callbacks = PassthroughSubject<RoomTimelineControllerCallback, Never>()
private(set) var timelineItems = [RoomTimelineItemProtocol]()
var roomId: String {
roomProxy.id
}
init(userId: String,
roomId: String,
roomProxy: RoomProxyProtocol,
timelineProvider: RoomTimelineProviderProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol,
roomProxy: RoomProxyProtocol) {
mediaProvider: MediaProviderProtocol) {
self.userId = userId
self.roomId = roomId
self.timelineProvider = timelineProvider
self.timelineItemFactory = timelineItemFactory
self.mediaProvider = mediaProvider
@@ -0,0 +1,30 @@
//
// 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 Foundation
struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol {
func buildRoomTimelineController(userId: String,
roomProxy: RoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) -> RoomTimelineControllerProtocol {
RoomTimelineController(userId: userId,
roomProxy: roomProxy,
timelineProvider: RoomTimelineProvider(roomProxy: roomProxy),
timelineItemFactory: timelineItemFactory,
mediaProvider: mediaProvider)
}
}
@@ -0,0 +1,25 @@
//
// 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 Foundation
@MainActor
protocol RoomTimelineControllerFactoryProtocol {
func buildRoomTimelineController(userId: String,
roomProxy: RoomProxyProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) -> RoomTimelineControllerProtocol
}
@@ -26,6 +26,7 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
private let userSession: UserSessionProtocol
private let navigationSplitCoordinator: NavigationSplitCoordinator
private let bugReportService: BugReportServiceProtocol
private let roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol
private let emojiProvider: EmojiProviderProtocol = EmojiProvider()
private let sidebarNavigationStackCoordinator: NavigationStackCoordinator
@@ -33,11 +34,15 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
var callback: ((UserSessionFlowCoordinatorAction) -> Void)?
init(userSession: UserSessionProtocol, navigationSplitCoordinator: NavigationSplitCoordinator, bugReportService: BugReportServiceProtocol) {
init(userSession: UserSessionProtocol,
navigationSplitCoordinator: NavigationSplitCoordinator,
bugReportService: BugReportServiceProtocol,
roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol) {
stateMachine = UserSessionFlowCoordinatorStateMachine()
self.userSession = userSession
self.navigationSplitCoordinator = navigationSplitCoordinator
self.bugReportService = bugReportService
self.roomTimelineControllerFactory = roomTimelineControllerFactory
sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
detailNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
@@ -150,13 +155,11 @@ class UserSessionFlowCoordinator: CoordinatorProtocol {
mediaProvider: userSession.mediaProvider,
roomProxy: roomProxy,
attributedStringBuilder: AttributedStringBuilder())
let timelineController = RoomTimelineController(userId: userId,
roomId: roomIdentifier,
timelineProvider: RoomTimelineProvider(roomProxy: roomProxy),
timelineItemFactory: timelineItemFactory,
mediaProvider: userSession.mediaProvider,
roomProxy: roomProxy)
let timelineController = roomTimelineControllerFactory.buildRoomTimelineController(userId: userId,
roomProxy: roomProxy,
timelineItemFactory: timelineItemFactory,
mediaProvider: userSession.mediaProvider)
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: detailNavigationStackCoordinator,
timelineController: timelineController,
@@ -36,6 +36,7 @@ enum UITestScreenIdentifier: String {
case roomSmallTimelineIncomingAndSmallPagination
case roomSmallTimelineLargePagination
case sessionVerification
case userSessionScreen
}
extension UITestScreenIdentifier: CustomStringConvertible {
@@ -18,58 +18,64 @@ import SwiftUI
import UIKit
class UITestsAppCoordinator: AppCoordinatorProtocol {
private var currentRootCoordinator: CoordinatorProtocol?
private let navigationStackCoordinator: NavigationStackCoordinator
let notificationManager: NotificationManagerProtocol? = nil
private let navigationRootCoordinator: NavigationRootCoordinator
private var mockScreens: [MockScreen] = []
var notificationManager: NotificationManagerProtocol?
init() {
UIView.setAnimationsEnabled(false)
navigationStackCoordinator = NavigationStackCoordinator()
navigationRootCoordinator = NavigationRootCoordinator()
mockScreens = UITestScreenIdentifier.allCases.map { MockScreen(id: $0, navigationRootCoordinator: navigationRootCoordinator) }
ServiceLocator.shared.register(userNotificationController: MockUserNotificationController())
}
func start() {
let screens = mockScreens()
let rootCoordinator = UITestsRootCoordinator(mockScreens: screens) { id in
guard let screen = screens.first(where: { $0.id == id }) else {
let rootCoordinator = UITestsRootCoordinator(mockScreens: mockScreens) { id in
guard let screen = self.mockScreens.first(where: { $0.id == id }) else {
fatalError()
}
// Store the initial coordinator so that it stays alive if drops it
// For example when replacing the root in the authentication flows
self.currentRootCoordinator = screen.coordinator
self.navigationStackCoordinator.setRootCoordinator(screen.coordinator)
self.navigationRootCoordinator.setRootCoordinator(screen.coordinator)
}
navigationStackCoordinator.setRootCoordinator(rootCoordinator)
navigationRootCoordinator.setRootCoordinator(rootCoordinator)
Bundle.elementFallbackLanguage = "en"
}
func toPresentable() -> AnyView {
navigationStackCoordinator.toPresentable()
}
private func mockScreens() -> [MockScreen] {
UITestScreenIdentifier.allCases.map { MockScreen(id: $0, navigationStackCoordinator: navigationStackCoordinator) }
func toPresentable() -> AnyView {
navigationRootCoordinator.toPresentable()
}
}
@MainActor
class MockScreen: Identifiable {
let id: UITestScreenIdentifier
let navigationStackCoordinator: NavigationStackCoordinator
private let navigationRootCoordinator: NavigationRootCoordinator
private var retainedState = [Any]()
init(id: UITestScreenIdentifier, navigationRootCoordinator: NavigationRootCoordinator) {
self.id = id
self.navigationRootCoordinator = navigationRootCoordinator
}
lazy var coordinator: CoordinatorProtocol = {
switch id {
case .login:
return LoginCoordinator(parameters: .init(authenticationService: MockAuthenticationServiceProxy(),
navigationStackCoordinator: navigationStackCoordinator))
let navigationStackCoordinator = NavigationStackCoordinator()
let coordinator = LoginCoordinator(parameters: .init(authenticationService: MockAuthenticationServiceProxy(),
navigationStackCoordinator: navigationStackCoordinator))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .serverSelection:
return ServerSelectionCoordinator(parameters: .init(authenticationService: MockAuthenticationServiceProxy(),
userNotificationController: MockUserNotificationController(),
isModallyPresented: true))
let navigationStackCoordinator = NavigationStackCoordinator()
let coordinator = ServerSelectionCoordinator(parameters: .init(authenticationService: MockAuthenticationServiceProxy(),
userNotificationController: MockUserNotificationController(),
isModallyPresented: true))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .serverSelectionNonModal:
return ServerSelectionCoordinator(parameters: .init(authenticationService: MockAuthenticationServiceProxy(),
userNotificationController: MockUserNotificationController(),
@@ -78,8 +84,12 @@ class MockScreen: Identifiable {
return AnalyticsPromptCoordinator(parameters: .init(userSession: MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@mock:client.com"),
mediaProvider: MockMediaProvider())))
case .authenticationFlow:
return AuthenticationCoordinator(authenticationService: MockAuthenticationServiceProxy(),
navigationStackCoordinator: navigationStackCoordinator)
let navigationStackCoordinator = NavigationStackCoordinator()
let coordinator = AuthenticationCoordinator(authenticationService: MockAuthenticationServiceProxy(),
navigationStackCoordinator: navigationStackCoordinator)
retainedState.append(coordinator)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .softLogout:
let credentials = SoftLogoutCredentials(userId: "@mock:matrix.org",
homeserverName: "matrix.org",
@@ -93,47 +103,66 @@ class MockScreen: Identifiable {
case .simpleUpgrade:
return TemplateCoordinator(parameters: .init(promptType: .upgrade))
case .home:
let navigationStackCoordinator = NavigationStackCoordinator()
let session = MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@mock:matrix.org"),
mediaProvider: MockMediaProvider())
return HomeScreenCoordinator(parameters: .init(userSession: session,
attributedStringBuilder: AttributedStringBuilder(),
bugReportService: MockBugReportService(),
navigationStackCoordinator: navigationStackCoordinator))
let coordinator = HomeScreenCoordinator(parameters: .init(userSession: session,
attributedStringBuilder: AttributedStringBuilder(),
bugReportService: MockBugReportService(),
navigationStackCoordinator: navigationStackCoordinator))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .settings:
return SettingsCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
userNotificationController: MockUserNotificationController(),
userSession: MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@mock:client.com"),
mediaProvider: MockMediaProvider()),
bugReportService: MockBugReportService()))
let navigationStackCoordinator = NavigationStackCoordinator()
let coordinator = SettingsCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
userNotificationController: MockUserNotificationController(),
userSession: MockUserSession(clientProxy: MockClientProxy(userIdentifier: "@mock:client.com"),
mediaProvider: MockMediaProvider()),
bugReportService: MockBugReportService()))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .bugReport:
return BugReportCoordinator(parameters: .init(bugReportService: MockBugReportService(),
userNotificationController: MockUserNotificationController(),
screenshot: nil,
isModallyPresented: false))
let navigationStackCoordinator = NavigationStackCoordinator()
let coordinator = BugReportCoordinator(parameters: .init(bugReportService: MockBugReportService(),
userNotificationController: MockUserNotificationController(),
screenshot: nil,
isModallyPresented: true))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .bugReportWithScreenshot:
return BugReportCoordinator(parameters: .init(bugReportService: MockBugReportService(),
userNotificationController: MockUserNotificationController(),
screenshot: Asset.Images.appLogo.image,
isModallyPresented: false))
let navigationStackCoordinator = NavigationStackCoordinator()
let coordinator = BugReportCoordinator(parameters: .init(bugReportService: MockBugReportService(),
userNotificationController: MockUserNotificationController(),
screenshot: Asset.Images.appLogo.image,
isModallyPresented: false))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .onboarding:
return OnboardingCoordinator()
case .roomPlainNoAvatar:
let navigationStackCoordinator = NavigationStackCoordinator()
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(),
roomName: "Some room name",
roomAvatarUrl: nil,
emojiProvider: EmojiProvider())
return RoomScreenCoordinator(parameters: parameters)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomEncryptedWithAvatar:
let navigationStackCoordinator = NavigationStackCoordinator()
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
timelineController: MockRoomTimelineController(),
mediaProvider: MockMediaProvider(),
roomName: "Some room name",
roomAvatarUrl: "mock_url",
emojiProvider: EmojiProvider())
return RoomScreenCoordinator(parameters: parameters)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomSmallTimeline:
let navigationStackCoordinator = NavigationStackCoordinator()
let timelineController = MockRoomTimelineController()
timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk
let parameters = RoomScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
@@ -142,8 +171,12 @@ class MockScreen: Identifiable {
roomName: "New room",
roomAvatarUrl: "mock_url",
emojiProvider: EmojiProvider())
return RoomScreenCoordinator(parameters: parameters)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomSmallTimelineIncomingAndSmallPagination:
let navigationStackCoordinator = NavigationStackCoordinator()
let timelineController = MockRoomTimelineController()
timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk
timelineController.backPaginationResponses = [RoomTimelineItemFixtures.singleMessageChunk]
@@ -155,8 +188,13 @@ class MockScreen: Identifiable {
roomName: "Small timeline",
roomAvatarUrl: "mock_url",
emojiProvider: EmojiProvider())
return RoomScreenCoordinator(parameters: parameters)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomSmallTimelineLargePagination:
let navigationStackCoordinator = NavigationStackCoordinator()
let timelineController = MockRoomTimelineController()
timelineController.timelineItems = RoomTimelineItemFixtures.smallChunk
timelineController.backPaginationResponses = [RoomTimelineItemFixtures.largeChunk]
@@ -166,15 +204,28 @@ class MockScreen: Identifiable {
roomName: "Small timeline, paginating",
roomAvatarUrl: "mock_url",
emojiProvider: EmojiProvider())
return RoomScreenCoordinator(parameters: parameters)
let coordinator = RoomScreenCoordinator(parameters: parameters)
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .sessionVerification:
let parameters = SessionVerificationCoordinatorParameters(sessionVerificationControllerProxy: MockSessionVerificationControllerProxy())
return SessionVerificationCoordinator(parameters: parameters)
case .userSessionScreen:
let navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: SplashScreenCoordinator())
let clientProxy = MockClientProxy(userIdentifier: "@mock:client.com", roomSummaryProvider: MockRoomSummaryProvider(state: .loaded))
let coordinator = UserSessionFlowCoordinator(userSession: MockUserSession(clientProxy: clientProxy, mediaProvider: MockMediaProvider()),
navigationSplitCoordinator: navigationSplitCoordinator,
bugReportService: MockBugReportService(),
roomTimelineControllerFactory: MockRoomTimelineControllerFactory())
coordinator.start()
retainedState.append(coordinator)
return navigationSplitCoordinator
}
}()
init(id: UITestScreenIdentifier, navigationStackCoordinator: NavigationStackCoordinator) {
self.id = id
self.navigationStackCoordinator = navigationStackCoordinator
}
}
@@ -31,8 +31,7 @@ struct UITestsRootCoordinator: CoordinatorProtocol {
}
.accessibilityIdentifier(coordinator.id.rawValue)
}
.padding(.top, 50) // Add some top padding so the iPad split screen button isn't tapped by mistake
.listStyle(.plain)
.navigationTitle("Screens")
.navigationViewStyle(.stack)
}
}
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2016 Nicholas Cross
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-1
View File
@@ -1 +0,0 @@
Files copied over from [nicholascross/WeakDictionary](https://github.com/nicholascross/WeakDictionary) because of a lack SPM support.
@@ -1,116 +0,0 @@
// MIT License
//
// Copyright (c) 2016 Nicholas Cross
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import Foundation
public struct WeakDictionary<Key: Hashable, Value: AnyObject> {
private var storage: [Key: WeakDictionaryReference<Value>]
public init() {
self.init(storage: [Key: WeakDictionaryReference<Value>]())
}
public init(dictionary: [Key: Value]) {
var newStorage = [Key: WeakDictionaryReference<Value>]()
dictionary.forEach { key, value in newStorage[key] = WeakDictionaryReference<Value>(value: value) }
self.init(storage: newStorage)
}
private init(storage: [Key: WeakDictionaryReference<Value>]) {
self.storage = storage
}
public mutating func reap() {
storage = weakDictionary().storage
}
public func weakDictionary() -> WeakDictionary<Key, Value> {
self[startIndex..<endIndex]
}
public func dictionary() -> [Key: Value] {
var newStorage = [Key: Value]()
storage.forEach { key, value in
if let retainedValue = value.value {
newStorage[key] = retainedValue
}
}
return newStorage
}
}
extension WeakDictionary: Collection {
public typealias Index = DictionaryIndex<Key, WeakDictionaryReference<Value>>
public var startIndex: Index {
storage.startIndex
}
public var endIndex: Index {
storage.endIndex
}
public func index(after index: Index) -> Index {
storage.index(after: index)
}
public subscript(position: Index) -> (Key, WeakDictionaryReference<Value>) {
return storage[position]
}
public subscript(key: Key) -> Value? {
get {
guard let valueRef = storage[key] else {
return nil
}
return valueRef.value
}
set {
guard let value = newValue else {
storage[key] = nil
return
}
storage[key] = WeakDictionaryReference<Value>(value: value)
}
}
public subscript(bounds: Range<Index>) -> WeakDictionary<Key, Value> {
let subStorage = storage[bounds.lowerBound..<bounds.upperBound]
var newStorage = [Key: WeakDictionaryReference<Value>]()
subStorage.filter { _, value in value.value != nil }
.forEach { key, value in newStorage[key] = value }
return WeakDictionary<Key, Value>(storage: newStorage)
}
}
public extension Dictionary where Value: AnyObject {
func weakDictionary() -> WeakDictionary<Key, Value> {
WeakDictionary<Key, Value>(dictionary: self)
}
}
@@ -1,53 +0,0 @@
// MIT License
//
// Copyright (c) 2016 Nicholas Cross
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import Foundation
public struct WeakDictionaryKey<Key: AnyObject & Hashable, Value: AnyObject>: Hashable {
private weak var baseKey: Key?
private let hash: Int
private var retainedValue: Value?
private let nilKeyHash = UUID().hashValue
public init(key: Key, value: Value? = nil) {
baseKey = key
retainedValue = value
hash = key.hashValue
}
public static func == (lhs: WeakDictionaryKey, rhs: WeakDictionaryKey) -> Bool {
(lhs.baseKey != nil && rhs.baseKey != nil && lhs.baseKey == rhs.baseKey)
|| lhs.hashValue == rhs.hashValue
}
public func hash(into hasher: inout Hasher) {
if baseKey == nil {
hasher.combine(nilKeyHash)
} else {
hasher.combine(baseKey)
}
}
public var key: Key? {
baseKey
}
}
@@ -1,35 +0,0 @@
// MIT License
//
// Copyright (c) 2016 Nicholas Cross
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import Foundation
public struct WeakDictionaryReference<Value: AnyObject> {
private weak var referencedValue: Value?
init(value: Value) {
referencedValue = value
}
public var value: Value? {
referencedValue
}
}
@@ -1,136 +0,0 @@
// MIT License
//
// Copyright (c) 2016 Nicholas Cross
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import Foundation
public struct WeakKeyDictionary<Key: AnyObject & Hashable, Value: AnyObject> {
private var storage: WeakDictionary<WeakDictionaryKey<Key, Value>, Value>
private let valuesRetainedByKey: Bool
public init(valuesRetainedByKey: Bool = false) {
self.init(
storage: WeakDictionary<WeakDictionaryKey<Key, Value>, Value>(),
valuesRetainedByKey: valuesRetainedByKey
)
}
public init(dictionary: [Key: Value], valuesRetainedByKey: Bool = false) {
var newStorage = WeakDictionary<WeakDictionaryKey<Key, Value>, Value>()
dictionary.forEach { key, value in
var keyRef: WeakDictionaryKey<Key, Value>!
if valuesRetainedByKey {
keyRef = WeakDictionaryKey<Key, Value>(key: key, value: value)
} else {
keyRef = WeakDictionaryKey<Key, Value>(key: key)
}
newStorage[keyRef] = value
}
self.init(storage: newStorage, valuesRetainedByKey: valuesRetainedByKey)
}
private init(storage: WeakDictionary<WeakDictionaryKey<Key, Value>, Value>, valuesRetainedByKey: Bool = false) {
self.storage = storage
self.valuesRetainedByKey = valuesRetainedByKey
}
public mutating func reap() {
storage = weakKeyDictionary().storage
}
public func weakDictionary() -> WeakDictionary<Key, Value> {
dictionary().weakDictionary()
}
public func weakKeyDictionary() -> WeakKeyDictionary<Key, Value> {
self[startIndex..<endIndex]
}
public func dictionary() -> [Key: Value] {
var newStorage = [Key: Value]()
storage.forEach { key, value in
if let retainedKey = key.key, let retainedValue = value.value {
newStorage[retainedKey] = retainedValue
}
}
return newStorage
}
}
extension WeakKeyDictionary: Collection {
public typealias Index = DictionaryIndex<WeakDictionaryKey<Key, Value>, WeakDictionaryReference<Value>>
public var startIndex: Index {
storage.startIndex
}
public var endIndex: Index {
storage.endIndex
}
public func index(after index: Index) -> Index {
storage.index(after: index)
}
public subscript(position: Index) -> (WeakDictionaryKey<Key, Value>, WeakDictionaryReference<Value>) {
return storage[position]
}
public subscript(key: Key) -> Value? {
get {
storage[WeakDictionaryKey<Key, Value>(key: key)]
}
set {
let retainedValue = valuesRetainedByKey ? newValue : nil
let weakKey = WeakDictionaryKey<Key, Value>(key: key, value: retainedValue)
storage[weakKey] = newValue
}
}
public subscript(bounds: Range<Index>) -> WeakKeyDictionary<Key, Value> {
let subStorage = storage[bounds.lowerBound..<bounds.upperBound]
var newStorage = WeakDictionary<WeakDictionaryKey<Key, Value>, Value>()
subStorage.filter { key, value in key.key != nil && value.value != nil }
.forEach { key, value in newStorage[key] = value.value }
return WeakKeyDictionary<Key, Value>(storage: newStorage)
}
}
public extension WeakDictionary where Key: AnyObject {
func weakKeyDictionary(valuesRetainedByKey: Bool = false) -> WeakKeyDictionary<Key, Value> {
WeakKeyDictionary<Key, Value>(dictionary: dictionary(), valuesRetainedByKey: valuesRetainedByKey)
}
}
public extension Dictionary where Key: AnyObject, Value: AnyObject {
func weakKeyDictionary(valuesRetainedByKey: Bool = false) -> WeakKeyDictionary<Key, Value> {
WeakKeyDictionary<Key, Value>(dictionary: self, valuesRetainedByKey: valuesRetainedByKey)
}
}
+1 -1
View File
@@ -128,7 +128,7 @@ targets:
sources:
- path: ../Sources
excludes:
- Screens/Templates
- Other/Extensions/XCUIElement.swift
- path: ../Resources
- path: ../SupportingFiles
- path: ../../Tools/Scripts/Templates/SimpleScreenExample/ElementX