Multi-State Transitions Developer Guide
Quick Referenceโ
Key Conceptsโ
Concept | Description |
---|---|
No Primary Target | All states in activate set are equal |
Path Node Eligibility | ANY activated state can be a path node |
Path Success | Only next path node needs activation |
IncomingTransition Execution | ALL activated states verify arrival |
Code Patternsโ
Defining Multi-State Transitionsโ
// Using JavaStateTransition Builder
JavaStateTransition multiActivation = new JavaStateTransition.Builder()
.setFunction(() -> action.click(loginButton).isSuccess())
.addToActivate("Dashboard") // All four states
.addToActivate("NavigationBar") // are activated
.addToActivate("StatusPanel") // equally - no
.addToActivate("NotificationArea") // primary target
.build();
Using @TransitionSet Patternโ
@TransitionSet(state = LoginState.class)
public class LoginTransitions {
@OutgoingTransition(to = ApplicationState.class)
public boolean openApplication() {
// This click activates multiple states defined in ApplicationState
return action.click(loginState.getLoginButton()).isSuccess();
}
}
Common Scenariosโ
1. Login Opens Entire Applicationโ
Scenario: Login button opens dashboard, navigation, sidebar, and status bar simultaneously.
// Transition definition
JavaStateTransition loginToApp = new JavaStateTransition.Builder()
.setFunction(() -> action.click(loginButton).isSuccess())
.addToActivate("Dashboard")
.addToActivate("Navigation")
.addToActivate("Sidebar")
.addToActivate("StatusBar")
.addToExit("LoginScreen") // Login screen disappears
.build();
Pathfinding Impact:
// After login, these paths become available:
// Login โ Dashboard โ AnyDashboardChild
// Login โ Navigation โ AnyNavChild
// Login โ Sidebar โ AnySidebarChild
// Login โ StatusBar โ AnyStatusChild
// Example: Reaching Settings via different routes
navigator.openState("Settings");
// Might use: Login โ Navigation โ Settings
// Or: Login โ Sidebar โ Settings
// Whichever path exists and is shortest
2. Tab Switching with Persistent Navigationโ
Scenario: Switching tabs changes content but keeps navigation active.
// Tab1 to Tab2 transition
JavaStateTransition switchToTab2 = new JavaStateTransition.Builder()
.setFunction(() -> action.click(tab2Button).isSuccess())
.addToActivate("Tab2Content")
.addToActivate("Tab2Sidebar") // Tab2 specific sidebar
.addToExit("Tab1Content") // Remove Tab1 content
.addToExit("Tab1Sidebar") // Remove Tab1 sidebar
// Note: Navigation NOT in exit list - stays active
.build();
3. Modal Dialog Over Contentโ
Scenario: Settings modal appears over dashboard without hiding it.
// Open modal keeping background visible
JavaStateTransition openSettings = new JavaStateTransition.Builder()
.setFunction(() -> action.click(settingsIcon).isSuccess())
.addToActivate("SettingsModal")
.addToActivate("ModalOverlay")
// Dashboard NOT in exit list - remains visible
.setStaysVisibleAfterTransition(StaysVisible.TRUE)
.build();
4. Contextual State Activationโ
Scenario: Different states activate based on user role or context.
public class ContextualTransition {
public Set<String> determineActivatedStates() {
Set<String> toActivate = new HashSet<>();
// Always activate base dashboard
toActivate.add("Dashboard");
// Role-specific panels
if (userRole.isAdmin()) {
toActivate.add("AdminPanel");
toActivate.add("SystemMonitor");
}
if (userRole.hasNotifications()) {
toActivate.add("NotificationPanel");
}
return toActivate;
}
public JavaStateTransition buildTransition() {
JavaStateTransition.Builder builder = new JavaStateTransition.Builder()
.setFunction(() -> action.click(enterButton).isSuccess());
// Add all contextual states
determineActivatedStates().forEach(builder::addToActivate);
return builder.build();
}
}
Pathfinding Examplesโ
Understanding Path Discoveryโ
// Given this state structure:
// StateA โ activates [B, C, D]
// StateB โ activates [E]
// StateC โ activates [F]
// StateD โ activates [G]
// StateE โ activates [Target]
// When navigating to Target:
navigator.openState("Target");
// Pathfinder explores:
// From A: Can reach B, C, D
// From B: Can reach E
// From E: Can reach Target โ
// Path found: A โ B โ E โ Target
// Even though A also activated C and D,
// the path only needs B to continue
Testing Multiple Pathsโ
@Test
public void testMultiplePathsToTarget() {
// Setup: Login activates [Dashboard, Menu]
// Dashboard can reach Settings
// Menu can also reach Settings
// Path 1: Via Dashboard
stateMemory.clear();
stateMemory.addActiveState("Login");
Paths paths = pathFinder.getPathsToState(
Set.of("Login"), "Settings"
);
// Should find both paths:
// Login โ Dashboard โ Settings
// Login โ Menu โ Settings
assertEquals(2, paths.getPaths().size());
}
Verification Patternsโ
Complete Activation Verificationโ
@TransitionSet(state = WorkspaceState.class)
public class WorkspaceTransitions {
@IncomingTransition
public boolean verifyCompleteActivation() {
// Check ALL expected components
boolean dashboardVisible = action.find(dashboard).isSuccess();
boolean navVisible = action.find(navigation).isSuccess();
boolean sidebarVisible = action.find(sidebar).isSuccess();
boolean statusVisible = action.find(statusBar).isSuccess();
if (!dashboardVisible || !navVisible || !sidebarVisible || !statusVisible) {
log.error("Workspace activation incomplete: " +
"Dashboard={}, Nav={}, Sidebar={}, Status={}",
dashboardVisible, navVisible, sidebarVisible, statusVisible);
return false;
}
return true;
}
}
Partial Activation Toleranceโ
@IncomingTransition
public boolean verifyWithTolerance() {
// Required components
boolean coreVisible = action.find(mainContent).isSuccess();
if (!coreVisible) {
log.error("Core content not visible - failing");
return false;
}
// Optional components
boolean sidebarVisible = action.find(sidebar).isSuccess();
if (!sidebarVisible) {
log.warn("Sidebar not visible - continuing anyway");
}
return coreVisible; // Accept partial success
}
Best Practicesโ
DO โ โ
- Group Related States
// Good: Logical grouping
.addToActivate("EmailEditor")
.addToActivate("EmailToolbar")
.addToActivate("RecipientPanel")
- Document Multi-Activation
/**
* Opens email composer with all panels.
* Activates: EmailEditor (main), EmailToolbar (top), RecipientPanel (side)
*/
- Test Path Variations
// Test that state can be reached via multiple paths
testPathViaMenu();
testPathViaDashboard();
testPathViaShortcut();
- Handle Activation Failures
if (!allStatesActivated()) {
log.warn("Partial activation - attempting recovery");
return attemptRecovery();
}
DON'T โโ
- Don't Assume Primary Target
// Wrong thinking:
// "Dashboard is the main state, others are secondary"
// Right thinking:
// "All activated states are equal for pathfinding"
- Don't Activate Unrelated States
// Bad: Unrelated states
.addToActivate("EmailComposer")
.addToActivate("StockTicker") // Unrelated!
.addToActivate("WeatherWidget") // Unrelated!
- Don't Ignore Partial Activation
// Bad: Ignoring failures
boolean result = action.click(button).isSuccess();
// Should verify all expected states activated
Debuggingโ
Enable Transition Loggingโ
# application.properties
logging.level.io.github.jspinak.brobot.navigation.transition=TRACE
Debug Outputโ
// Check what states a transition activates
StateTransition transition = getTransition();
System.out.println("Activates: " + transition.getActivate());
System.out.println("Exits: " + transition.getExit());
// Verify joint table indexing
Set<Long> parents = jointTable.getIncomingTransitions(stateId);
System.out.println("Can be reached from: " + parents);
// Trace path finding
Paths paths = pathFinder.getPathsToState(activeStates, target);
paths.getPaths().forEach(System.out::println);
Summary Checklistโ
When implementing multi-state transitions:
- All activated states have IncomingTransition methods
- States are logically related (appear together in UI)
- Documentation lists all activated states
- Tests verify all states activate correctly
- Pathfinding tested through different activated states
- Partial activation handled gracefully
- Exit states explicitly specified when needed
- Visibility behavior (staysVisible) set appropriately