mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 16:24:11 +00:00
Re-write the space flow state machine configuration and cover it more in UI tests. (#5098)
* Use the old state machine configuration style. * Test starting the space settings flow in UI tests. * Test starting the create space room flow in UI tests.
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -252,6 +252,7 @@ struct CreateRoomScreen: View {
|
||||
ToolbarButton(role: .cancel) {
|
||||
context.send(viewAction: .dismiss)
|
||||
}
|
||||
.accessibilityIdentifier(A11yIdentifiers.createRoomScreen.cancel)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ struct SpaceSettingsScreen: View {
|
||||
private var editSection: some View {
|
||||
Section {
|
||||
ListRow(kind: .custom { editRow })
|
||||
.accessibilityIdentifier(A11yIdentifiers.spaceSettingsScreen.editBaseInfo)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a9a76ebfb64858e3e501e7e83c5f807dafc77b4b0e973190809a2240045c973a
|
||||
size 264477
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b4bed76c5b0fd4ab4ef243a7b4966ce1a8d0396771407ddd6905eb0498895cfe
|
||||
size 307993
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6d6d275b6bd0e5cbbd5b4c2400ab9bfc5b9c5ecc851149266792ea62f9903018
|
||||
size 120898
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:638dd34b225e913bb4d13f8191ccac8a15b8f7f106668e859aa7656fd5e66761
|
||||
size 154018
|
||||
Reference in New Issue
Block a user