diff --git a/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift index b94f56c24..a63aecb05 100644 --- a/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift @@ -195,161 +195,153 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { // MARK: - Private - // swiftlint:disable:next function_body_length cyclomatic_complexity + // swiftlint:disable:next function_body_length private func configureStateMachine() { - stateMachine.addRoutes(event: .start, transitions: [.initial => .space]) { [weak self] _ in - self?.presentSpace() - } - - stateMachine.addRoutes(event: .startUnjoined, transitions: [.initial => .joinSpace]) { [weak self] _ in - self?.presentJoinSpaceScreen() - } - - stateMachine.addRoutes(event: .joinedSpace, transitions: [.joinSpace => .space]) { [weak self] _ in - self?.presentSpaceAfterJoining() - } - stateMachine.addRoutes(event: .leftSpace, transitions: [.space => .leftSpace]) { [weak self] _ in - self?.clearRoute(animated: true) - } - - stateMachine.addRoutes(event: .addRooms, transitions: [.space => .addingRooms]) { [weak self] _ in - self?.presentSpaceAddRoomsScreen() - } - stateMachine.addRoutes(event: .dismissedAddRooms, transitions: [.addingRooms => .space]) - - stateMachine.addRoutes(event: .presentTransferOwnership, transitions: [.space => .transferOwnership]) { [weak self] context in - guard let self, let roomProxy = context.userInfo as? JoinedRoomProxyProtocol else { return } - self.presentTransferOwnershipScreen(roomProxy: roomProxy) - } - stateMachine.addRoutes(event: .dismissedTransferOwnership, transitions: [.transferOwnership => .space]) - stateMachine.addRouteMapping { event, fromState, userInfo in - guard event == .startChildFlow else { return nil } - guard let childEntryPoint = userInfo as? SpaceFlowCoordinatorEntryPoint else { fatalError("An entry point must be provided.") } - return switch fromState { - case .space: .presentingChild(childSpaceID: childEntryPoint.spaceID, previousState: fromState) - case .roomFlow(let previousState): .presentingChild(childSpaceID: childEntryPoint.spaceID, previousState: previousState) - default: nil - } - } handler: { [weak self] context in - guard let self, let entryPoint = context.userInfo as? SpaceFlowCoordinatorEntryPoint else { return } - startChildFlow(with: entryPoint) - } - - stateMachine.addRouteMapping { event, fromState, _ in - guard event == .stopChildFlow, case .presentingChild(_, let previousState) = fromState else { return nil } - return previousState - } handler: { [weak self] _ in - guard let self else { return } - childSpaceFlowCoordinator = nil - selectedSpaceRoomSubject.send(nil) - } - - stateMachine.addRouteMapping { event, fromState, _ in - guard case .startRoomFlow = event, case .roomFlow = fromState else { + switch (fromState, event) { + case (.initial, .start): + return .space + case (.initial, .startUnjoined): + return .joinSpace + + case (.joinSpace, .joinedSpace): + return .space + case (.space, .leftSpace): + return .leftSpace + + case (.space, .addRooms): + return .addingRooms + case (.addingRooms, .dismissedAddRooms): + return .space + + case (.space, .presentTransferOwnership): + return .transferOwnership + case (.transferOwnership, .dismissedTransferOwnership): + return .space + + case (.space, .startChildFlow): + guard let childEntryPoint = userInfo as? SpaceFlowCoordinatorEntryPoint else { fatalError("An entry point must be provided.") } + return .presentingChild(childSpaceID: childEntryPoint.spaceID, previousState: fromState) + case (.roomFlow(let previousState), .startChildFlow): + guard let childEntryPoint = userInfo as? SpaceFlowCoordinatorEntryPoint else { fatalError("An entry point must be provided.") } + return .presentingChild(childSpaceID: childEntryPoint.spaceID, previousState: previousState) + case (.presentingChild(_, let previousState), .stopChildFlow): + return previousState + + case (.space, .startRoomFlow): + return .roomFlow(previousState: fromState) + case (.roomFlow, .startRoomFlow): + return fromState // Ignore tapping on multiple rooms at the same time + case (.roomFlow(let previousState), .stopRoomFlow): + return previousState + + case (.space, .startMembersFlow): + return .membersFlow + case (.membersFlow, .stopMembersFlow): + return .space + + case (.space, .startSettingsFlow): + return .settingsFlow + case (.settingsFlow, .stopSettingsFlow): + return .space + + case (.space, .startRolesAndPermissionsFlow): + return .rolesAndPermissionsFlow + case (.rolesAndPermissionsFlow, .stopRolesAndPermissionsFlow): + return .space + + case (.space, .startCreateChildRoomFlow): + return .createChildRoomFlow + case (.createChildRoomFlow, .stopCreateChildRoomFlow): + return .space + + default: return nil } + } + + stateMachine.addAnyHandler(.any => .any) { [weak self] context in + guard let self else { return } + + switch (context.fromState, context.event, context.toState) { + case (.initial, .start, .space): + presentSpace() + case (.initial, .startUnjoined, .joinSpace): + presentJoinSpaceScreen() + + case (.joinSpace, .joinedSpace, .space): + presentSpaceAfterJoining() + case (.space, .leftSpace, .leftSpace): + clearRoute(animated: true) + + case (.space, .addRooms, .addingRooms): + presentSpaceAddRoomsScreen() + case (.addingRooms, .dismissedAddRooms, .space): + break + + case (.space, .presentTransferOwnership, .transferOwnership): + guard let roomProxy = context.userInfo as? JoinedRoomProxyProtocol else { return } + presentTransferOwnershipScreen(roomProxy: roomProxy) + case (.transferOwnership, .dismissedTransferOwnership, .space): + break + + case (.space, .startChildFlow, .presentingChild), + (.roomFlow, .startChildFlow, .presentingChild): + guard let entryPoint = context.userInfo as? SpaceFlowCoordinatorEntryPoint else { return } + startChildFlow(with: entryPoint) + case (.presentingChild, .stopChildFlow, _): + childSpaceFlowCoordinator = nil + selectedSpaceRoomSubject.send(nil) + + case (.space, .startRoomFlow(let roomID), .roomFlow): + startRoomFlow(roomID: roomID) + case (.roomFlow, .startRoomFlow, .roomFlow): + break // Ignore tapping on multiple rooms at the same time + case (.roomFlow, .stopRoomFlow, _): + roomFlowCoordinator = nil + selectedSpaceRoomSubject.send(nil) + + case (.space, .startMembersFlow, .membersFlow): + guard let roomProxy = context.userInfo as? JoinedRoomProxyProtocol else { + fatalError("The room proxy must always be provided") + } + startMembersFlow(roomProxy: roomProxy) + case (.membersFlow, .stopMembersFlow, .space): + membersFlowCoordinator = nil + + case (.space, .startSettingsFlow, .settingsFlow): + guard let roomProxy = context.userInfo as? JoinedRoomProxyProtocol else { return } + startSettingsFlow(roomProxy: roomProxy) + case (.settingsFlow, .stopSettingsFlow, .space): + settingsFlowCoordinator = nil + + case (.space, .startRolesAndPermissionsFlow, .rolesAndPermissionsFlow): + guard let roomProxy = context.userInfo as? JoinedRoomProxyProtocol else { return } + startRolesAndPermissionsFlow(roomProxy: roomProxy) + case (.rolesAndPermissionsFlow, .stopRolesAndPermissionsFlow, .space): + rolesAndPermissionsFlowCoordinator = nil + + case (.space, .startCreateChildRoomFlow, .createChildRoomFlow): + guard let space = context.userInfo as? SpaceServiceRoom else { fatalError("The space is missing") } + startCreateChildFlow(space: space) + case (.createChildRoomFlow, .stopCreateChildRoomFlow, .space): + createChildRoomFlowCoordinator = nil + + default: + break + } - return fromState - } handler: { _ in - // Ignore tapping on multiple rooms at the same time - } - - stateMachine.addRouteMapping { event, fromState, _ in - guard case .startRoomFlow = event, case .space = fromState else { return nil } - return .roomFlow(previousState: fromState) - } handler: { [weak self] context in - guard let self, case let .startRoomFlow(roomID) = context.event else { return } - startRoomFlow(roomID: roomID) - } - - stateMachine.addRouteMapping { event, fromState, _ in - guard event == .stopRoomFlow, case let .roomFlow(previousState) = fromState else { return nil } - return previousState - } handler: { [weak self] _ in - guard let self else { return } - roomFlowCoordinator = nil - selectedSpaceRoomSubject.send(nil) - } - - stateMachine.addRouteMapping { event, fromState, _ in - guard case .startMembersFlow = event, case .space = fromState else { - return nil - } - return .membersFlow - } handler: { [weak self] context in - guard let self, let roomProxy = context.userInfo as? JoinedRoomProxyProtocol else { - fatalError("The room proxy must always be provided") - } - startMembersFlow(roomProxy: roomProxy) - } - - stateMachine.addRouteMapping { event, fromState, _ in - guard event == .stopMembersFlow, case .membersFlow = fromState else { return nil } - return .space - } handler: { [weak self] _ in - guard let self else { return } - membersFlowCoordinator = nil - } - - stateMachine.addRouteMapping { event, fromState, _ in - guard event == .startSettingsFlow, case .space = fromState else { return nil } - return .settingsFlow - } handler: { [weak self] context in - guard let self, let roomProxy = context.userInfo as? JoinedRoomProxyProtocol else { return } - startSettingsFlow(roomProxy: roomProxy) - } - - stateMachine.addRouteMapping { event, fromState, _ in - guard event == .stopSettingsFlow, case .settingsFlow = fromState else { return nil } - return .space - } handler: { [weak self] _ in - guard let self else { return } - settingsFlowCoordinator = nil - } - - stateMachine.addRouteMapping { event, fromState, _ in - guard event == .startRolesAndPermissionsFlow, case .space = fromState else { return nil } - return .rolesAndPermissionsFlow - } handler: { [weak self] context in - guard let self, let roomProxy = context.userInfo as? JoinedRoomProxyProtocol else { return } - startRolesAndPermissionsFlow(roomProxy: roomProxy) - } - - stateMachine.addRouteMapping { event, fromState, _ in - guard event == .stopRolesAndPermissionsFlow, case .rolesAndPermissionsFlow = fromState else { return nil } - return .space - } handler: { [weak self] _ in - guard let self else { return } - rolesAndPermissionsFlowCoordinator = nil - } - - stateMachine.addRouteMapping { event, fromState, _ in - guard event == .startCreateChildRoomFlow, case .space = fromState else { return nil } - return .createChildRoomFlow - } handler: { [weak self] context in - guard let space = context.userInfo as? SpaceServiceRoom else { fatalError("The space is missing") } - self?.startCreateChildFlow(space: space) - } - - stateMachine.addRouteMapping { event, fromState, _ in - guard event == .stopCreateChildRoomFlow, case .createChildRoomFlow = fromState else { return nil } - return .space - } handler: { [weak self] _ in - self?.createChildRoomFlowCoordinator = nil - } - - stateMachine.addErrorHandler { context in - fatalError("Unexpected transition: \(context)") - } - - stateMachine.addAnyHandler(.any => .any) { context in + // Log transitions if let event = context.event { MXLog.info("Transitioning from `\(context.fromState)` to `\(context.toState)` with event `\(event)`") } else { MXLog.info("Transitioning from \(context.fromState)` to `\(context.toState)`") } } + + stateMachine.addErrorHandler { context in + fatalError("Unexpected transition: \(context)") + } } private func presentSpace() { diff --git a/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift b/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift index 5849a8ae0..d9f89fc64 100644 --- a/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift +++ b/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift @@ -22,6 +22,7 @@ extension SpaceServiceProxyMock { var joinedParentSpaces: [SpaceServiceRoom] = [] var editableSpaces: [SpaceServiceRoom] = [] var spaceRoomLists: [String: SpaceRoomListProxyMock] = [:] + var spaceRooms: [SpaceServiceRoom] = [] var leaveSpaceRooms: [LeaveSpaceRoom] = [] } @@ -46,7 +47,8 @@ extension SpaceServiceProxyMock { leaveHandle: LeaveSpaceHandleSDKMock(.init(rooms: configuration.leaveSpaceRooms)))) } spaceForIdentifierSpaceIDClosure = { spaceID in - .success(configuration.topLevelSpaces.first { $0.id == spaceID }) + let space = configuration.topLevelSpaces.first { $0.id == spaceID } ?? configuration.spaceRooms.first { $0.id == spaceID } + return .success(space) } addChildToReturnValue = .success(()) removeChildFromReturnValue = .success(()) @@ -70,6 +72,7 @@ extension SpaceServiceProxyMock.Configuration { return .init(topLevelSpaces: .mockJoinedSpaces, spaceFilters: spaceFilters, - spaceRoomLists: .init(uniqueKeysWithValues: spaceRoomLists + subSpaceRoomLists)) + spaceRoomLists: .init(uniqueKeysWithValues: spaceRoomLists + subSpaceRoomLists), + spaceRooms: .mockSpaceList) } } diff --git a/ElementX/Sources/Other/AccessibilityIdentifiers.swift b/ElementX/Sources/Other/AccessibilityIdentifiers.swift index 3db68bd2d..c95326d4c 100644 --- a/ElementX/Sources/Other/AccessibilityIdentifiers.swift +++ b/ElementX/Sources/Other/AccessibilityIdentifiers.swift @@ -48,6 +48,7 @@ enum A11yIdentifiers { static let spacesScreen = SpacesScreen() static let spaceScreen = SpaceScreen() static let spaceAddRoomsScreen = SpaceAddRoomsScreen() + static let spaceSettingsScreen = SpaceSettingsScreen() static let linkNewDeviceScreen = LinkNewDeviceScreen() struct AlertInfo { @@ -276,6 +277,7 @@ enum A11yIdentifiers { struct CreateRoomScreen { let create = "create_room-create" + let cancel = "create_room-cancel" let roomAvatar = "create_room-room_avatar" let roomName = "create_room-room_name" let roomTopic = "create_room-room_topic" @@ -328,14 +330,20 @@ enum A11yIdentifiers { struct SpaceScreen { let moreMenu = "space_screen-more_menu" + let createRoom = "space_screen-create_room" let addExistingRooms = "space_screen-add_existing_rooms" let viewMembers = "space_screen-view_members" + let settings = "space_screen-settings" } struct SpaceAddRoomsScreen { let cancel = "space_add_rooms_screen-cancel" } + struct SpaceSettingsScreen { + let editBaseInfo = "space_settings_screen-edit_space_info" + } + struct LinkNewDeviceScreen { let cancel = "link_new_device_screen-cancel" let mobileDevice = "link_new_device_screen-mobile_device" diff --git a/ElementX/Sources/Screens/CreateRoomScreen/View/CreateRoomScreen.swift b/ElementX/Sources/Screens/CreateRoomScreen/View/CreateRoomScreen.swift index 00d4f6c3f..caafc5add 100644 --- a/ElementX/Sources/Screens/CreateRoomScreen/View/CreateRoomScreen.swift +++ b/ElementX/Sources/Screens/CreateRoomScreen/View/CreateRoomScreen.swift @@ -252,6 +252,7 @@ struct CreateRoomScreen: View { ToolbarButton(role: .cancel) { context.send(viewAction: .dismiss) } + .accessibilityIdentifier(A11yIdentifiers.createRoomScreen.cancel) } } diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift index 2fb0cbb3f..ac739eb7b 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift @@ -132,7 +132,9 @@ struct SpaceScreen: View { Button { context.send(viewAction: .createChildRoom) } label: { Label(L10n.actionCreateRoom, icon: \.plus) } + .accessibilityIdentifier(A11yIdentifiers.spaceScreen.createRoom) } + Button { context.send(viewAction: .addExistingRooms) } label: { Label(L10n.actionAddExistingRooms, icon: \.room) } @@ -165,6 +167,7 @@ struct SpaceScreen: View { Button { context.send(viewAction: .spaceSettings(roomProxy: roomProxy)) } label: { Label(L10n.commonSettings, icon: \.settings) } + .accessibilityIdentifier(A11yIdentifiers.spaceScreen.settings) } } diff --git a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/View/SpaceSettingsScreen.swift b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/View/SpaceSettingsScreen.swift index a3fb3d6ea..3b0f67f12 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/View/SpaceSettingsScreen.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/View/SpaceSettingsScreen.swift @@ -30,6 +30,7 @@ struct SpaceSettingsScreen: View { private var editSection: some View { Section { ListRow(kind: .custom { editRow }) + .accessibilityIdentifier(A11yIdentifiers.spaceSettingsScreen.editBaseInfo) } } diff --git a/UITests/Sources/UserSessionScreenTests.swift b/UITests/Sources/UserSessionScreenTests.swift index 7ab325851..d87442617 100644 --- a/UITests/Sources/UserSessionScreenTests.swift +++ b/UITests/Sources/UserSessionScreenTests.swift @@ -29,6 +29,8 @@ class UserSessionScreenTests: XCTestCase { static let spaceJoinRoomScreen = 9 static let spaceAddRoomsScreen = 10 static let spaceMembersListScreen = 11 + static let spaceSettingsScreen = 12 + static let createSpaceRoomScreen = 13 } func testUserSessionFlows() async throws { @@ -96,6 +98,15 @@ class UserSessionScreenTests: XCTestCase { try await Task.sleep(for: .seconds(1)) try await app.assertScreenshot(step: Step.subspaceScreen) + app.buttons[A11yIdentifiers.spaceScreen.moreMenu].tap() + app.buttons[A11yIdentifiers.spaceScreen.createRoom].tap() + XCTAssertTrue(app.buttons[A11yIdentifiers.createRoomScreen.cancel].waitForExistence(timeout: 5.0)) + try await Task.sleep(for: .seconds(1)) + try await app.assertScreenshot(step: Step.createSpaceRoomScreen) + + app.buttons[A11yIdentifiers.createRoomScreen.cancel].tap() + XCTAssert(app.staticTexts[joinedSubspaceName].waitForExistence(timeout: 5.0)) + app.buttons[A11yIdentifiers.spaceScreen.moreMenu].tap() app.buttons[A11yIdentifiers.spaceScreen.addExistingRooms].tap() XCTAssert(app.buttons[A11yIdentifiers.spaceAddRoomsScreen.cancel].waitForExistence(timeout: 5.0)) @@ -114,6 +125,15 @@ class UserSessionScreenTests: XCTestCase { app.navigationBars.buttons[joinedSubspaceName].firstMatch.tap(.center) XCTAssert(app.staticTexts[joinedSubspaceName].waitForExistence(timeout: 5.0)) + app.buttons[A11yIdentifiers.spaceScreen.moreMenu].tap() + app.buttons[A11yIdentifiers.spaceScreen.settings].tap() + XCTAssert(app.buttons[A11yIdentifiers.spaceSettingsScreen.editBaseInfo].waitForExistence(timeout: 5.0)) + try await Task.sleep(for: .seconds(1)) + try await app.assertScreenshot(step: Step.spaceSettingsScreen) + + app.navigationBars.buttons[joinedSubspaceName].firstMatch.tap(.center) + XCTAssert(app.staticTexts[joinedSubspaceName].waitForExistence(timeout: 5.0)) + app.buttons[A11yIdentifiers.spacesScreen.spaceRoomName(joinedSubspaceRoomName)].tap() XCTAssert(app.buttons[joinedSubspaceRoomName].waitForExistence(timeout: 5.0)) try await Task.sleep(for: .seconds(1)) diff --git a/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPad-en-GB-12.png b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPad-en-GB-12.png new file mode 100644 index 000000000..dbee4595b --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPad-en-GB-12.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9a76ebfb64858e3e501e7e83c5f807dafc77b4b0e973190809a2240045c973a +size 264477 diff --git a/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPad-en-GB-13.png b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPad-en-GB-13.png new file mode 100644 index 000000000..b17abd687 --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPad-en-GB-13.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4bed76c5b0fd4ab4ef243a7b4966ce1a8d0396771407ddd6905eb0498895cfe +size 307993 diff --git a/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPhone-en-GB-12.png b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPhone-en-GB-12.png new file mode 100644 index 000000000..1a02c43fa --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPhone-en-GB-12.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d6d275b6bd0e5cbbd5b4c2400ab9bfc5b9c5ecc851149266792ea62f9903018 +size 120898 diff --git a/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPhone-en-GB-13.png b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPhone-en-GB-13.png new file mode 100644 index 000000000..9d823af0f --- /dev/null +++ b/UITests/Sources/__Snapshots__/Application/userSessionScreen.testSpaceExploration-iPhone-en-GB-13.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:638dd34b225e913bb4d13f8191ccac8a15b8f7f106668e859aa7656fd5e66761 +size 154018