Status: Work in Progress
Last updated: 2026-02-19
ReactJS and Single-Page Application (SPA) frontends present unique challenges for automation tools like Whistleblower. This guide covers strategies for reliably capturing screenshots and DOM state from React-based BAS/SCADA interfaces.
Before diving into theory, grab a template:
- react-url-based.template.json - For sites where URLs change during navigation (✅ EASIEST, try this first)
- react-click-based.template.json - For sites where URL stays same but views change (requires selector work)
- trane-tracer-synchrony.template.json - Meatball Tracers login + baseline captures
- ignition_perspective_annotated.example.json - Detailed Ignition Perspective example
Workflow: Run bootstrap_recorder.py → Check if generated config has multiple URLs → Pick template → Test → Adjust timing
See REACT-QUICK-REF.md for copy-paste config patterns.
React applications often generate:
- Auto-generated class names (e.g.,
css-1x2y3z4,sc-AbCdEf) - Dynamic component identifiers
- Randomly hashed attributes
- Deeply nested component hierarchies
- Components mount/unmount dynamically
- Data fetches happen after initial page load
- Loading states replace actual content
- Hydration delays on client-side rendering
- URL changes don't trigger full page reloads
- Navigation happens via JavaScript (React Router, etc.)
page.goto()may not trigger expected state changes- Browser considers navigation "aborted" (
net::ERR_ABORTED)
- UI updates based on WebSocket/SSE data
- State changes triggered by timers
- Conditional rendering based on application state
React apps often expose stable data attributes for testing:
{
"selector": "[data-testid='dashboard-panel']",
"selector_candidates": [
"[data-testid='dashboard-panel']",
"[data-component='dashboard']",
"#main-dashboard"
]
}Priority order:
data-testid,data-test,data-cy(test attributes)data-component,data-view(semantic attributes)idattributes (if stable)- ARIA attributes:
[aria-label="Dashboard"],[role="main"] - Text content:
text=Dashboard(use as last resort)
❌ Don't use:
- Generated class names:
.css-1x2y3z4,.MuiBox-root-123 - Deep nth-child chains:
div:nth-child(3) > div:nth-child(2) - Position-dependent selectors without context
✅ Do use:
- Semantic HTML:
nav,main,article,section - Stable custom attributes:
[data-component-path],[data-widget-id] - Combined stable selectors:
nav[aria-label="Main"] button[aria-label="Dashboard"]
The built-in wait_for_target_ready() function checks for:
- Element exists in DOM
- Not showing "loading..." text
- Document ready state is "complete"
- Network activity has settled
Enhance loading detection in your config:
{
"name": "react_dashboard",
"url": "https://your-spa.example.com/#/dashboard",
"root_selector": "main[role='main']",
"settle_ms": 15000,
"screenshot_full_page": false,
"pre_click_steps": [
{
"selector": "[data-testid='loading-indicator']",
"action": "wait_for_hidden",
"wait_ms": 5000
}
]
}React apps often need more time to hydrate and fetch data:
{
"settle_ms": 15000, // ← 15 seconds for complex dashboards
"timeout_ms": 120000 // ← 2 minutes for slow networks
}The bootstrap_recorder.py tool automatically handles many React challenges:
python3 bootstrap_recorder.py \
--url "https://your-react-app.example.com/" \
--site-name "react_scada" \
--viewport-width 1920 \
--viewport-height 1080 \
--record-videoWhat it captures:
- Click events with multiple selector candidates
- Form field changes (including React controlled inputs)
- Navigation events and URL changes
- Timing between actions
sites/react_scada.bootstrap.json - Basic config
sites/react_scada.steps.json - Suggested pre-click steps with:
- Multiple selector candidates
- Inferred wait times based on observed delays
- Action types (click, dblclick)
- Element text for verification
-
Review selector candidates:
{ "selector_candidates": [ "div[data-component='ia.display.label'][data-component-path='C$0:0:0$0:1.0:0:0:0:1:1']", "text=AHU 2" ] }Choose the most stable selector (usually data attributes first).
-
Adjust wait times: Bootstrap recorder infers timing from your actions. Increase for slower environments:
{ "wait_ms": 2000 // ← Increase to 3000-5000 for React hydration } -
Add loading state checks: Manually add steps to wait for loaders to disappear.
React Router and similar libraries use hash-based navigation (#/dashboard). Whistleblower handles this automatically:
page.goto("https://app.example.com/#/dashboard")
# Browser may abort navigation with net::ERR_ABORTED
# because the page is already loadedWhistleblower's goto_with_hash_abort_tolerance() function:
- Catches
net::ERR_ABORTEDerrors - Polls for URL changes
- Waits for the React Router to update
You don't need to do anything special - this is handled automatically.
Use pre_click_steps to navigate within the app:
{
"name": "dashboard",
"url": "https://app.example.com/",
"root_selector": "main",
"pre_click_steps": [
{
"selector": "nav a[href='#/dashboard']",
"action": "click",
"wait_ms": 3000
}
]
}For deeply nested views (tree navigation, tab switching, modal opening):
{
"name": "nested_equipment_view",
"url": "https://app.example.com/#/equipment",
"root_selector": "[data-view='equipment-detail']",
"settle_ms": 12000,
"pre_click_steps": [
{
"selector": "[data-tree-node='building-1']",
"action": "click",
"nth": 0,
"wait_ms": 2000,
"comment": "Expand building node in tree"
},
{
"selector": "[data-tree-node='floor-2']",
"action": "click",
"nth": 0,
"wait_ms": 2000,
"comment": "Expand floor node"
},
{
"selector": "[data-equipment-id='AHU-01']",
"action": "dblclick",
"nth": 0,
"wait_ms": 5000,
"comment": "Open equipment detail with double-click"
}
]
}Key points:
- Each step can have its own
wait_ms - Use
nthto select specific elements in a list actioncan beclickordblclick- Comments are ignored by Whistleblower but help with maintenance
When selectors are completely unstable, use Playwright's code generator:
npx playwright codegen \
"https://your-react-app.example.com/" \
--viewport-size 1920,1080This opens an interactive browser where you can:
- Click through your navigation flow
- See generated selectors in real-time
- Copy stable selectors to your config
- Test selectors in the Playwright inspector
Save the session:
npx playwright codegen \
"https://your-react-app.example.com/" \
--viewport-size 1920,1080 \
-o codegen-session.tsThen extract selectors from codegen-session.ts.
Problem: Generated class names like .MuiButton-root-123
Solution: Use data attributes or ARIA labels:
{
"selector": "button[aria-label='Open Dashboard']",
"selector_candidates": [
"button[aria-label='Open Dashboard']",
"button[data-testid='dashboard-btn']",
"text=Dashboard"
]
}Solution: Use .ant- prefixed stable classes:
{
"selector": ".ant-menu-item[data-menu-id='dashboard']",
"selector_candidates": [
".ant-menu-item[data-menu-id='dashboard']",
".ant-menu-item:has-text('Dashboard')"
]
}Ignition's Perspective framework uses data-component attributes:
{
"selector": "div[data-component='ia.display.label'][data-component-path='C$0:0:0$0:1.0:0:0:0:1:1']",
"selector_candidates": [
"div[data-component='ia.display.label'][data-component-path='C$0:0:0$0:1.0:0:0:0:1:1']",
"text=AHU 2"
]
}Note: data-component-path values are often stable across sessions.
If you control the React app, add test attributes:
// Your React component
function DashboardPanel() {
return (
<div data-testid="dashboard-panel" data-component="dashboard">
<h1 data-testid="dashboard-title">Building Status</h1>
{/* ... */}
</div>
);
}Then use in Whistleblower:
{
"selector": "[data-testid='dashboard-panel']"
}Diagnosis:
- Check if element exists: Open DevTools and test selector
- Is it in a shadow DOM? (Not currently supported)
- Is it in an iframe? (Add iframe handling)
- Does it load after initial render?
Solutions:
- Increase
settle_msto 15000+ - Add
pre_click_stepsto wait for loading indicators - Use broader selectors:
[data-component*="dashboard"]instead of exact match
Diagnosis: Element exists but content isn't loaded yet.
Solutions:
{
"settle_ms": 20000,
"pre_click_steps": [
{
"selector": "[data-loading='true']",
"action": "wait_for_hidden",
"wait_ms": 10000
}
]
}Diagnosis:
Hash routing causing ERR_ABORTED.
Solutions:
- Let auto-retry handle it (already implemented)
- Navigate via clicks instead of
url:{ "url": "https://app.example.com/", // ← Base URL only "pre_click_steps": [ { "selector": "nav a[href='#/target']", "action": "click", "wait_ms": 3000 } ] }
Diagnosis: React re-renders may change element references or component keys.
Solutions:
- Use attribute selectors instead of class names
- Add retry logic via multiple
selector_candidates - Check if element is conditionally rendered
{
"name": "react_simple",
"base_url": "https://dashboard.example.com/",
"ignore_https_errors": false,
"login_attempts": 2,
"viewport": {
"width": 1920,
"height": 1080
},
"login": {
"username": "${REACT_USER}",
"password": "${REACT_PASS}",
"user_selector": "input[name='username']",
"pass_selector": "input[name='password']",
"submit_selector": "button[type='submit']",
"success_selector": "[data-testid='user-menu']"
},
"watch": [
{
"name": "main_dashboard",
"url": "https://dashboard.example.com/#/dashboard",
"root_selector": "main[role='main']",
"settle_ms": 12000,
"screenshot_full_page": true
}
]
}{
"name": "react_tree_nav",
"base_url": "https://scada.example.com/",
"ignore_https_errors": true,
"login_attempts": 2,
"viewport": {
"width": 1920,
"height": 1080
},
"login": {
"username": "${SCADA_USER}",
"password": "${SCADA_PASS}",
"user_selector": "#email",
"pass_selector": "#password",
"submit_selector": "button[aria-label='Log In']",
"success_selector": "[aria-label='User Settings']"
},
"watch": [
{
"name": "equipment_detail",
"url": "https://scada.example.com/#/equipment",
"root_selector": "[data-view='equipment-detail']",
"settle_ms": 15000,
"screenshot_full_page": false,
"screenshot_selector": "[data-panel='equipment-info']",
"pre_click_steps": [
{
"selector": "[data-tree-expand][data-node-id='site-main']",
"action": "click",
"nth": 0,
"wait_ms": 2000
},
{
"selector": "[data-tree-node='building-hvac']",
"action": "click",
"nth": 0,
"wait_ms": 2000
},
{
"selector": "[data-equipment-card='AHU-01']",
"action": "dblclick",
"nth": 0,
"wait_ms": 5000
}
]
}
]
}Based on the actual BAS_DEMOclick config in the repository:
{
"name": "ignition_perspective",
"base_url": "https://demo.inductiveautomation.com/data/perspective/client/building-management-system-demo/",
"ignore_https_errors": true,
"login_attempts": 2,
"viewport": {
"width": 1920,
"height": 1080
},
"login": {
"username": "",
"password": "",
"user_selector": "#username",
"pass_selector": "#password",
"submit_selector": "button[type='submit']",
"success_selector": "body"
},
"watch": [
{
"name": "ahu_2_detail",
"url": "https://demo.inductiveautomation.com/data/perspective/client/building-management-system-demo/",
"root_selector": "body",
"settle_ms": 10000,
"screenshot_full_page": false,
"pre_click_steps": [
{
"selector": "div[data-component='ia.display.label'][data-component-path='C$0:0:0$0:1.0:0:0:0:1:1']",
"selector_candidates": [
"div[data-component='ia.display.label'][data-component-path='C$0:0:0$0:1.0:0:0:0:1:1']",
"text=AHU 2"
],
"action": "click",
"nth": 0,
"wait_ms": 2000
}
]
}
]
}- Always use
bootstrap_recorder.pyfirst - Let it discover selectors automatically - Prefer data attributes over class names or position-based selectors
- Test selectors in DevTools before adding to config
- Increase
settle_msto 12000-20000 for React apps - Use multiple
selector_candidatesas fallbacks - Add pre-click steps for multi-stage navigation
- Handle loading states explicitly
- Document complex selectors with comments in your JSON (strip before use)
- Test on slow networks to verify timeouts are sufficient
- Review bootstrap suggestions - don't blindly trust auto-generated selectors
Potential enhancements being considered:
- Auto-detect and wait for React hydration
- Smarter loading state detection (spinner patterns, skeleton screens)
- Support for shadow DOM elements
- Iframe navigation helpers
- Selector stability scoring
- Auto-fallback through selector candidates
- React DevTools integration for component path discovery
- REACT-TROUBLESHOOTING.md - Step-by-step troubleshooting checklist
- REACT-QUICK-REF.md - Quick reference with copy-paste snippets
bootstrap_recorder.py- Record sessions and generate configswhistleblower.py- Main capture enginesites/example.json- Config templatesites/ignition_perspective_annotated.example.json- Annotated real-world exampleREADME.md- Bootstrap documentation
If you encounter React-specific issues not covered here:
- Run
bootstrap_recorder.pywith--record-videoto capture the session - Review
data/bootstrap/<site>/events.jsonfor captured interactions - Check
sites/<site>.steps.jsonfor suggested selectors - Test selectors in browser DevTools console
- Increase
settle_msandwait_msvalues incrementally
Remember: Whistleblower is read-only. Never use selectors that trigger control actions or setpoint changes.