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:
Doug
2026-02-12 13:19:20 +00:00
committed by GitHub
parent 92a5fb395c
commit b34832f7f8
11 changed files with 186 additions and 146 deletions
@@ -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))
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a9a76ebfb64858e3e501e7e83c5f807dafc77b4b0e973190809a2240045c973a
size 264477
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b4bed76c5b0fd4ab4ef243a7b4966ce1a8d0396771407ddd6905eb0498895cfe
size 307993
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6d6d275b6bd0e5cbbd5b4c2400ab9bfc5b9c5ecc851149266792ea62f9903018
size 120898
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:638dd34b225e913bb4d13f8191ccac8a15b8f7f106668e859aa7656fd5e66761
size 154018