diff --git a/RiotSwiftUI/Modules/Common/Mock/MockScreen.swift b/RiotSwiftUI/Modules/Common/Mock/MockScreen.swift new file mode 100644 index 000000000..55adc3cb7 --- /dev/null +++ b/RiotSwiftUI/Modules/Common/Mock/MockScreen.swift @@ -0,0 +1,58 @@ +// +// Copyright 2021 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 + +/* + Used for mocking top level screens and their various state. + */ +@available(iOS 14.0, *) +protocol MockScreen { + associatedtype ScreenType: View + static func screen(for state: Self) -> ScreenType + static var screenStates: [Self] { get } +} + + +@available(iOS 14.0, *) +extension MockScreen { + + /* + Get a list of the screens for every screen state. + */ + static var screens: [ScreenType] { + Self.screenStates.map(screen(for:)) + } + + /* + Render each of the screen states in a group applying + any optional environment variables. + */ + static func screenGroup( + themeId: ThemeIdentifier = .light, + locale: Locale = Locale.current, + sizeCategory: ContentSizeCategory = ContentSizeCategory.medium + ) -> some View { + Group { + ForEach(0.. { - $presence.eraseToAnyPublisher() - } + private(set) var presenceSubject: CurrentValueSubject // MARK: - Setup init(session: MXSession) { self.session = session + self.presenceSubject = CurrentValueSubject(TemplateUserProfilePresence(mxPresence: session.myUser.presence)) self.listenerReference = setupPresenceListener() } @@ -65,7 +62,7 @@ class TemplateUserProfileService: TemplateUserProfileServiceProtocol { let event = event, case .presence = MXEventType(identifier: event.eventId) else { return } - self.presence = TemplateUserProfilePresence(mxPresence: self.session.myUser.presence) + self.presenceSubject.send(TemplateUserProfilePresence(mxPresence: self.session.myUser.presence)) } // TODO: Add log back when abstract logger added to RiotSwiftUI // if reference == nil { diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateProfileUserScreen.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateProfileUserScreen.swift new file mode 100644 index 000000000..a23c630a5 --- /dev/null +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateProfileUserScreen.swift @@ -0,0 +1,45 @@ +// +// Copyright 2021 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 +import SwiftUI + +/** + Using an enum for the screen allows you define the different state cases with + the relevant associated data for each case. + */ +@available(iOS 14.0, *) +enum MockTemplateProfileUserScreenStates: MockScreen { + + case mockPresenceStates(TemplateUserProfilePresence) + case mockLongDisplayName(String) + + static var screenStates: [MockTemplateProfileUserScreenStates] = TemplateUserProfilePresence.allCases.map(MockTemplateProfileUserScreenStates.mockPresenceStates) + + [.mockLongDisplayName("Somebody with a super long name we would like to test")] + + static func screen(for state: MockTemplateProfileUserScreenStates) -> some View { + let service: MockTemplateUserProfileService + switch state { + case .mockPresenceStates(let presence): + service = MockTemplateUserProfileService(presence: presence) + case .mockLongDisplayName(let displayName): + service = MockTemplateUserProfileService(displayName: displayName) + } + let viewModel = TemplateUserProfileViewModel(userService: service) + return TemplateUserProfile(viewModel: viewModel) + .addDependency(MockAvatarService.example) + } +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileService.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileService.swift index 1f6a8f570..9a713fedc 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileService.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/Mock/MockTemplateUserProfileService.swift @@ -19,20 +19,24 @@ import Combine @available(iOS 14.0, *) class MockTemplateUserProfileService: TemplateUserProfileServiceProtocol { - - static let example = MockTemplateUserProfileService() - static let initialPresenceState: TemplateUserProfilePresence = .offline - @Published var presence: TemplateUserProfilePresence = initialPresenceState - var presencePublisher: AnyPublisher { - $presence.eraseToAnyPublisher() + var presenceSubject: CurrentValueSubject + + let userId: String + var displayName: String? + let avatarUrl: String? + init( + userId: String = "123", + displayName: String? = "Alice", + avatarUrl: String? = "mx123@matrix.com", + presence: TemplateUserProfilePresence = .offline + ) { + self.userId = userId + self.displayName = displayName + self.avatarUrl = avatarUrl + self.presenceSubject = CurrentValueSubject(presence) } - let userId: String = "123" - let displayName: String? = "Alice" - let avatarUrl: String? = "mx123@matrix.com" - let currentlyActive: Bool = true - let lastActive: UInt = 1630596918513 func simulateUpdate(presence: TemplateUserProfilePresence) { - self.presence = presence + self.presenceSubject.send(presence) } } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/TemplateUserProfileServiceProtocol.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/TemplateUserProfileServiceProtocol.swift index 581d7cc37..452a6b037 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/TemplateUserProfileServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Service/TemplateUserProfileServiceProtocol.swift @@ -22,7 +22,7 @@ protocol TemplateUserProfileServiceProtocol: Avatarable { var userId: String { get } var displayName: String? { get } var avatarUrl: String? { get } - var presencePublisher: AnyPublisher { get } + var presenceSubject: CurrentValueSubject { get } } @available(iOS 14.0, *) diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/UI/TestUserProfileUITests.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/UI/TestUserProfileUITests.swift new file mode 100644 index 000000000..5ec87e61d --- /dev/null +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/UI/TestUserProfileUITests.swift @@ -0,0 +1,47 @@ +// +// Copyright 2021 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 + +@testable import RiotSwiftUI + +@available(iOS 14.0, *) +class TestUserProfileUITests: XCTestCase { + + let app = XCUIApplication() + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. + app.launch() + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testUserContentTextDisplayed() throws { + let userContentText = app.staticTexts["More great user content!"] + XCTAssert(userContentText.exists) + } + +} diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/TemplateUserProfileViewModelTests.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/Unit/TemplateUserProfileViewModelTests.swift similarity index 69% rename from RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/TemplateUserProfileViewModelTests.swift rename to RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/Unit/TemplateUserProfileViewModelTests.swift index cff2dba86..e7724c6d8 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/TemplateUserProfileViewModelTests.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/Test/Unit/TemplateUserProfileViewModelTests.swift @@ -17,27 +17,30 @@ import XCTest import Combine -@testable import Riot +@testable import RiotSwiftUI @available(iOS 14.0, *) class TemplateUserProfileViewModelTests: XCTestCase { + private enum Constants { + static let presenceInitialValue: TemplateUserProfilePresence = .offline + static let displayName = "Alice" + } var service: MockTemplateUserProfileService! var viewModel: TemplateUserProfileViewModel! var cancellables = Set() override func setUpWithError() throws { - service = MockTemplateUserProfileService() + service = MockTemplateUserProfileService(displayName: Constants.displayName, presence: Constants.presenceInitialValue) viewModel = TemplateUserProfileViewModel(userService: service) } func testInitialState() { - XCTAssertEqual(viewModel.viewState.displayName, MockTemplateUserProfileService.example.displayName) - XCTAssertEqual(viewModel.viewState.avatar?.mxContentUri, MockTemplateUserProfileService.example.avatarUrl) - XCTAssertEqual(viewModel.viewState.presence, MockTemplateUserProfileService.initialPresenceState) + XCTAssertEqual(viewModel.viewState.displayName, Constants.displayName) + XCTAssertEqual(viewModel.viewState.presence, Constants.presenceInitialValue) } func testFirstPresenceRecieved() throws { let presencePublisher = viewModel.$viewState.map(\.presence).removeDuplicates().collect(1).first() - XCTAssertEqual(try xcAwait(presencePublisher), [MockTemplateUserProfileService.initialPresenceState]) + XCTAssertEqual(try xcAwait(presencePublisher), [Constants.presenceInitialValue]) } func testPresenceUpdatesRecieved() throws { @@ -46,8 +49,6 @@ class TemplateUserProfileViewModelTests: XCTestCase { let newPresenceValue2: TemplateUserProfilePresence = .idle service.simulateUpdate(presence: newPresenceValue1) service.simulateUpdate(presence: newPresenceValue2) - XCTAssertEqual(try xcAwait(presencePublisher), [MockTemplateUserProfileService.initialPresenceState, newPresenceValue1, newPresenceValue2]) + XCTAssertEqual(try xcAwait(presencePublisher), [Constants.presenceInitialValue, newPresenceValue1, newPresenceValue2]) } - - } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift index b3f059f2b..47582f2cd 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/View/TemplateUserProfile.swift @@ -45,6 +45,7 @@ struct TemplateUserProfile: View { } .frame(maxHeight: .infinity) } + .background(theme.colors.background) .frame(maxHeight: .infinity) .navigationTitle(viewModel.viewState.displayName ?? "") .navigationBarItems(leading: leftButton, trailing: rightButton) @@ -70,7 +71,6 @@ struct TemplateUserProfile: View { @available(iOS 14.0, *) struct TemplateUserProfile_Previews: PreviewProvider { static var previews: some View { - TemplateUserProfile(viewModel: TemplateUserProfileViewModel(userService: MockTemplateUserProfileService.example)) - .addDependency(MockAvatarService.example) + MockTemplateProfileUserScreenStates.screenGroup() } } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift index 56de3caf7..e64536381 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/ViewModel/TemplateUserProfileViewModel.swift @@ -36,7 +36,7 @@ class TemplateUserProfileViewModel: ObservableObject, TemplateUserProfileViewMod self.userService = userService self.viewState = initialState ?? Self.defaultState(userService: userService) - userService.presencePublisher + userService.presenceSubject .map(TemplateUserProfileStateAction.updatePresence) .receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] action in @@ -46,7 +46,7 @@ class TemplateUserProfileViewModel: ObservableObject, TemplateUserProfileViewMod } private static func defaultState(userService: TemplateUserProfileServiceProtocol) -> TemplateUserProfileViewState { - return TemplateUserProfileViewState(avatar: userService.avatarData, displayName: userService.displayName, presence: .offline) + return TemplateUserProfileViewState(avatar: userService.avatarData, displayName: userService.displayName, presence: userService.presenceSubject.value) } // MARK: - Public diff --git a/RiotSwiftUI/RiotSwiftUIApp.swift b/RiotSwiftUI/RiotSwiftUIApp.swift index 417ae1872..478d6f5d4 100644 --- a/RiotSwiftUI/RiotSwiftUIApp.swift +++ b/RiotSwiftUI/RiotSwiftUIApp.swift @@ -16,8 +16,7 @@ import SwiftUI /** - Just needed so the application target has an entry point for the moment. - Could use to render the different screens. + RiotSwiftUI screens rendered for UI Tests. */ @available(iOS 14.0, *) @main diff --git a/RiotSwiftUI/targetUITests.yml b/RiotSwiftUI/targetUITests.yml new file mode 100644 index 000000000..187c7c635 --- /dev/null +++ b/RiotSwiftUI/targetUITests.yml @@ -0,0 +1,62 @@ +name: RiotSwiftUITests + +schemes: + RiotSwiftUITests: + analyze: + config: Debug + archive: + config: Release + build: + targets: + RiotSwiftUITests: + - running + - testing + - profiling + - analyzing + - archiving + profile: + config: Release + run: + config: Debug + disableMainThreadChecker: true + test: + config: Debug + disableMainThreadChecker: true + targets: + - RiotSwiftUITests + +targets: + RiotSwiftUITests: + type: bundle.ui-testing + platform: iOS + + dependencies: + - target: RiotSwiftUI + +# configFiles: +# Debug: Debug.xcconfig +# Release: Release.xcconfig + + settings: + base: + TEST_TARGET_NAME: RiotSwiftUI +# PRODUCT_NAME: RiotSwiftUITests + PRODUCT_BUNDLE_IDENTIFIER: org.matrix.RiotSwiftUITests$(rfc1034identifier) +# BUNDLE_LOADER: $(TEST_HOST) +# FRAMEWORK_SEARCH_PATHS: $(SDKROOT)/Developer/Library/Frameworks $(inherited) +# INFOPLIST_FILE: RiotSwiftUI/Info.plist +# LD_RUNPATH_SEARCH_PATHS: $(inherited) @executable_path/Frameworks @loader_path/Frameworks +# PRODUCT_BUNDLE_IDENTIFIER: org.matrix.$(PRODUCT_NAME:rfc1034identifier) +# PRODUCT_NAME: RiotSwiftUITests +# TEST_TARGET_NAME: $(BUILT_PRODUCTS_DIR)/RiotSwiftUI.app/RiotSwiftUI +# configs: +# Debug: +# Release: +# PROVISIONING_PROFILE: $(RIOT_PROVISIONING_PROFILE) +# PROVISIONING_PROFILE_SPECIFIER: $(RIOT_PROVISIONING_PROFILE_SPECIFIER) + sources: + - path: ../RiotSwiftUI/Modules + includes: + - "**/Test" + excludes: + - "**/Test/Unit/**" diff --git a/RiotSwiftUI/targetUnitTests.yml b/RiotSwiftUI/targetUnitTests.yml new file mode 100644 index 000000000..dbf54400c --- /dev/null +++ b/RiotSwiftUI/targetUnitTests.yml @@ -0,0 +1,57 @@ +name: RiotSwiftUnitTests + +schemes: + RiotSwiftUnitTests: + analyze: + config: Debug + archive: + config: Release + build: + targets: + RiotSwiftUnitTests: + - running + - testing + - profiling + - analyzing + - archiving + profile: + config: Release + run: + config: Debug + disableMainThreadChecker: true + test: + config: Debug + disableMainThreadChecker: true + targets: + - RiotSwiftUnitTests + +targets: + RiotSwiftUnitTests: + type: bundle.unit-test + platform: iOS + + dependencies: + - target: RiotSwiftUI + + configFiles: + Debug: Debug.xcconfig + Release: Release.xcconfig + + settings: + base: + FRAMEWORK_SEARCH_PATHS: $(SDKROOT)/Developer/Library/Frameworks $(inherited) + INFOPLIST_FILE: RiotSwiftUI/Info.plist + LD_RUNPATH_SEARCH_PATHS: $(inherited) @executable_path/Frameworks @loader_path/Frameworks + PRODUCT_BUNDLE_IDENTIFIER: org.matrix.$(PRODUCT_NAME:rfc1034identifier) + PRODUCT_NAME: RiotSwiftUnitTests + configs: + Debug: + Release: + PROVISIONING_PROFILE: $(RIOT_PROVISIONING_PROFILE) + PROVISIONING_PROFILE_SPECIFIER: $(RIOT_PROVISIONING_PROFILE_SPECIFIER) + sources: + - path: ../RiotSwiftUI/Modules + includes: + - "**/Test" + excludes: + - "**/Test/UI/**" diff --git a/RiotTests/target.yml b/RiotTests/target.yml index ebf31a1d6..c32e1c8d9 100644 --- a/RiotTests/target.yml +++ b/RiotTests/target.yml @@ -65,6 +65,3 @@ targets: - path: ../Riot/Managers/EncryptionKeyManager/EncryptionKeyManager.swift - path: ../Riot/Managers/KeyValueStorage/ - path: ../Riot/PropertyWrappers/UserDefaultsBackedPropertyWrapper.swift - - path: ../RiotSwiftUI/Modules - includes: - - "**/Test/**" diff --git a/project.yml b/project.yml index 3f825be46..44c6eeb12 100644 --- a/project.yml +++ b/project.yml @@ -33,3 +33,5 @@ include: - path: RiotNSE/target.yml - path: DesignKit/target.yml - path: RiotSwiftUI/target.yml + - path: RiotSwiftUI/targetUnitTests.yml + - path: RiotSwiftUI/targetUITests.yml